From 993346b9aee07168708ff444136cb08c8b8b6542 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Wed, 13 Sep 2023 15:00:50 +0530 Subject: [PATCH] chore: rewrite survey editor to server components (#728) * created a new service for survey data mutation * made requested changes * made some refactors * ran pnpm format * removed console logs * removed some unused code * made upateSurvey return TSurvey and added laoding state to AddNoCodeActionModal * fixed minor bugs * ran pnpm format * fixed build issues * Replaced old question types with new types * fix survey list not up to date on changes * solved back button issue --------- Co-authored-by: Matthias Nannt --- .../actions/AddNoCodeActionModal.tsx | 25 ++- .../[environmentId]/surveys/PreviewSurvey.tsx | 2 - .../[environmentId]/surveys/SurveyList.tsx | 2 +- .../(analysis)/summary/CTASummary.tsx | 4 +- .../(analysis)/summary/ConsentSummary.tsx | 4 +- .../summary/MultipleChoiceSummary.tsx | 12 +- .../(analysis)/summary/NPSSummary.tsx | 4 +- .../(analysis)/summary/OpenTextSummary.tsx | 4 +- .../(analysis)/summary/RatingSummary.tsx | 5 +- .../(analysis)/summary/SummaryList.tsx | 35 ++-- .../[surveyId]/edit/AdvancedSettings.tsx | 7 +- .../[surveyId]/edit/CTAQuestionForm.tsx | 7 +- .../[surveyId]/edit/ConsentQuestionForm.tsx | 7 +- .../[surveyId]/edit/EditThankYouCard.tsx | 10 +- .../surveys/[surveyId]/edit/HowToSendCard.tsx | 16 +- .../surveys/[surveyId]/edit/LogicEditor.tsx | 12 +- .../edit/MultipleChoiceMultiForm.tsx | 7 +- .../edit/MultipleChoiceSingleForm.tsx | 7 +- .../[surveyId]/edit/NPSQuestionForm.tsx | 7 +- .../[surveyId]/edit/OpenQuestionForm.tsx | 7 +- .../surveys/[surveyId]/edit/QuestionCard.tsx | 4 +- .../surveys/[surveyId]/edit/QuestionsView.tsx | 15 +- .../[surveyId]/edit/RatingQuestionForm.tsx | 7 +- .../[surveyId]/edit/RecontactOptionsCard.tsx | 6 +- .../[surveyId]/edit/ResponseOptionsCard.tsx | 25 ++- .../surveys/[surveyId]/edit/SettingsView.tsx | 35 ++-- .../surveys/[surveyId]/edit/SurveyEditor.tsx | 120 ++++++------ .../surveys/[surveyId]/edit/SurveyMenuBar.tsx | 99 +++++----- .../surveys/[surveyId]/edit/Validation.ts | 14 +- .../[surveyId]/edit/WhenToSendCard.tsx | 82 +++++--- .../surveys/[surveyId]/edit/WhoToSendCard.tsx | 46 ++--- .../surveys/[surveyId]/edit/actions.ts | 12 ++ .../surveys/[surveyId]/edit/loading.tsx | 27 +++ .../surveys/[surveyId]/edit/page.tsx | 30 ++- .../surveys/templates/TemplateContainer.tsx | 1 - .../surveys/templates/templates.ts | 4 +- apps/web/app/s/[surveyId]/page.tsx | 7 +- apps/web/components/preview/TabOption.tsx | 2 +- packages/lib/services/actionClass.ts | 1 - packages/lib/services/attributeClass.ts | 1 - packages/lib/services/survey.ts | 182 ++++++++++++++++++ packages/types/v1/surveys.ts | 6 +- 42 files changed, 597 insertions(+), 313 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/loading.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx index 086ca8aeea..e8cb2db5c8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx @@ -8,8 +8,11 @@ import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { testURLmatch } from "./testURLmatch"; import { createActionClass } from "@formbricks/lib/services/actionClass"; -import { TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses"; -import { useRouter } from "next/navigation"; +import { + TActionClassInput, + TActionClassNoCodeConfig, + TActionClass, +} from "@formbricks/types/v1/actionClasses"; import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/CssSelector"; import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/PageUrlSelector"; import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/InnerHtmlSelector"; @@ -18,10 +21,15 @@ interface AddNoCodeActionModalProps { environmentId: string; open: boolean; setOpen: (v: boolean) => void; + setActionClassArray?; } -export default function AddNoCodeActionModal({ environmentId, open, setOpen }: AddNoCodeActionModalProps) { - const router = useRouter(); +export default function AddNoCodeActionModal({ + environmentId, + open, + setOpen, + setActionClassArray, +}: AddNoCodeActionModalProps) { const { register, control, handleSubmit, watch, reset } = useForm(); const [isPageUrl, setIsPageUrl] = useState(false); const [isCssSelector, setIsCssSelector] = useState(false); @@ -75,8 +83,13 @@ export default function AddNoCodeActionModal({ environmentId, open, setOpen }: A type: "noCode", } as TActionClassInput; - await createActionClass(environmentId, updatedData); - router.refresh(); + const newActionClass: TActionClass = await createActionClass(environmentId, updatedData); + if (setActionClassArray) { + setActionClassArray((prevActionClassArray: TActionClass[]) => [ + ...prevActionClassArray, + newActionClass, + ]); + } reset(); resetAllStates(false); toast.success("Action added successfully."); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx index a25b8ef3af..38e58bc87a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx @@ -1,5 +1,4 @@ "use client"; - import Modal from "@/components/preview/Modal"; import TabOption from "@/components/preview/TabOption"; import { SurveyInline } from "@/components/shared/Survey"; @@ -16,7 +15,6 @@ interface PreviewSurveyProps { survey: TSurvey | Survey; setActiveQuestionId: (id: string | null) => void; activeQuestionId?: string | null; - environmentId: string; previewType?: "modal" | "fullwidth" | "email"; product: TProduct; environment: TEnvironment; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx index 64147ef5cb..ef99ad31cc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx @@ -21,7 +21,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st const environment = await getEnvironment(environmentId); const surveys: TSurveyWithAnalytics[] = await getSurveysWithAnalytics(environmentId); const environments: TEnvironment[] = await getEnvironments(product.id); - const otherEnvironment = environments.find((e) => e.type !== environment.type); + const otherEnvironment = environments.find((e) => e.type !== environment.type)!; const totalSubmissions = surveys.reduce((acc, survey) => acc + (survey.analytics?.numResponses || 0), 0); if (surveys.length === 0) { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/CTASummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/CTASummary.tsx index 076c2a7eb3..819369a076 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/CTASummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/CTASummary.tsx @@ -1,11 +1,11 @@ -import { CTAQuestion } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; +import { TSurveyCTAQuestion } from "@formbricks/types/v1/surveys"; import { ProgressBar } from "@formbricks/ui"; import { InboxStackIcon } from "@heroicons/react/24/solid"; import { useMemo } from "react"; interface CTASummaryProps { - questionSummary: QuestionSummary; + questionSummary: QuestionSummary; } interface ChoiceResult { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/ConsentSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/ConsentSummary.tsx index 3e2c27b424..b5d5a68d7e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/ConsentSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/ConsentSummary.tsx @@ -1,11 +1,11 @@ -import { ConsentQuestion } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; import { ProgressBar } from "@formbricks/ui"; import { InboxStackIcon } from "@heroicons/react/24/solid"; import { useMemo } from "react"; +import { TSurveyConsentQuestion } from "@formbricks/types/v1/surveys"; interface ConsentSummaryProps { - questionSummary: QuestionSummary; + questionSummary: QuestionSummary; } interface ChoiceResult { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/MultipleChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/MultipleChoiceSummary.tsx index ae7ed75feb..c0f8277ac5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/MultipleChoiceSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/MultipleChoiceSummary.tsx @@ -1,17 +1,17 @@ -import { - MultipleChoiceMultiQuestion, - MultipleChoiceSingleQuestion, - QuestionType, -} from "@formbricks/types/questions"; +import { QuestionType } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; import { PersonAvatar, ProgressBar } from "@formbricks/ui"; import { InboxStackIcon } from "@heroicons/react/24/solid"; import { useMemo } from "react"; import Link from "next/link"; import { truncate } from "@/lib/utils"; +import { + TSurveyMultipleChoiceMultiQuestion, + TSurveyMultipleChoiceSingleQuestion, +} from "@formbricks/types/v1/surveys"; interface MultipleChoiceSummaryProps { - questionSummary: QuestionSummary; + questionSummary: QuestionSummary; environmentId: string; surveyType: string; } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/NPSSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/NPSSummary.tsx index 9bf94ce3e2..c06f4f7cae 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/NPSSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/NPSSummary.tsx @@ -1,11 +1,11 @@ -import { NPSQuestion } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; +import { TSurveyNPSQuestion } from "@formbricks/types/v1/surveys"; import { HalfCircle, ProgressBar } from "@formbricks/ui"; import { InboxStackIcon } from "@heroicons/react/24/solid"; import { useMemo } from "react"; interface NPSSummaryProps { - questionSummary: QuestionSummary; + questionSummary: QuestionSummary; } interface Result { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/OpenTextSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/OpenTextSummary.tsx index 12f290845d..1a0427b339 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/OpenTextSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/OpenTextSummary.tsx @@ -1,13 +1,13 @@ import { truncate } from "@/lib/utils"; import { timeSince } from "@formbricks/lib/time"; -import { OpenTextQuestion } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; +import { TSurveyOpenTextQuestion } from "@formbricks/types/v1/surveys"; import { PersonAvatar } from "@formbricks/ui"; import { InboxStackIcon } from "@heroicons/react/24/solid"; import Link from "next/link"; interface OpenTextSummaryProps { - questionSummary: QuestionSummary; + questionSummary: QuestionSummary; environmentId: string; } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/RatingSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/RatingSummary.tsx index 28d796f76f..a3e417bcac 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/RatingSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/RatingSummary.tsx @@ -3,10 +3,11 @@ import { ProgressBar } from "@formbricks/ui"; import { InboxStackIcon } from "@heroicons/react/24/solid"; import { useMemo } from "react"; import { RatingResponse } from "../RatingResponse"; -import { QuestionType, RatingQuestion } from "@formbricks/types/questions"; +import { QuestionType } from "@formbricks/types/questions"; +import { TSurveyRatingQuestion } from "@formbricks/types/v1/surveys"; interface RatingSummaryProps { - questionSummary: QuestionSummary; + questionSummary: QuestionSummary; } interface ChoiceResult { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SummaryList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SummaryList.tsx index 29cb3b0f2e..6940e68cfe 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SummaryList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SummaryList.tsx @@ -1,18 +1,19 @@ import ConsentSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/ConsentSummary"; import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; -import { - QuestionType, - type CTAQuestion, - type ConsentQuestion, - type MultipleChoiceMultiQuestion, - type MultipleChoiceSingleQuestion, - type NPSQuestion, - type OpenTextQuestion, - type RatingQuestion, -} from "@formbricks/types/questions"; +import { QuestionType } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; import { TResponse } from "@formbricks/types/v1/responses"; -import { TSurvey, TSurveyQuestion } from "@formbricks/types/v1/surveys"; +import { + TSurvey, + TSurveyCTAQuestion, + TSurveyConsentQuestion, + TSurveyMultipleChoiceMultiQuestion, + TSurveyMultipleChoiceSingleQuestion, + TSurveyNPSQuestion, + TSurveyOpenTextQuestion, + TSurveyQuestion, + TSurveyRatingQuestion, +} from "@formbricks/types/v1/surveys"; import CTASummary from "./CTASummary"; import MultipleChoiceSummary from "./MultipleChoiceSummary"; import NPSSummary from "./NPSSummary"; @@ -58,7 +59,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar return ( } + questionSummary={questionSummary as QuestionSummary} environmentId={environmentId} /> ); @@ -72,7 +73,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar key={questionSummary.question.id} questionSummary={ questionSummary as QuestionSummary< - MultipleChoiceMultiQuestion | MultipleChoiceSingleQuestion + TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion > } environmentId={environmentId} @@ -84,7 +85,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar return ( } + questionSummary={questionSummary as QuestionSummary} /> ); } @@ -92,7 +93,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar return ( } + questionSummary={questionSummary as QuestionSummary} /> ); } @@ -100,7 +101,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar return ( } + questionSummary={questionSummary as QuestionSummary} /> ); } @@ -108,7 +109,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar return ( } + questionSummary={questionSummary as QuestionSummary} /> ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AdvancedSettings.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AdvancedSettings.tsx index 53d2e4ddb3..d264e0d575 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AdvancedSettings.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AdvancedSettings.tsx @@ -1,13 +1,12 @@ import React from "react"; import LogicEditor from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/LogicEditor"; import UpdateQuestionId from "./UpdateQuestionId"; -import { Question } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; +import { TSurveyQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; interface AdvancedSettingsProps { - question: Question; + question: TSurveyQuestion; questionIdx: number; - localSurvey: Survey; + localSurvey: TSurveyWithAnalytics; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; } 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 27321878ca..98de1a8893 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 @@ -1,15 +1,14 @@ "use client"; import { md } from "@formbricks/lib/markdownIt"; -import type { CTAQuestion } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; +import { TSurveyCTAQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Editor, Input, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui"; import { useState } from "react"; import { BackButtonInput } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard"; interface CTAQuestionFormProps { - localSurvey: Survey; - question: CTAQuestion; + localSurvey: TSurveyWithAnalytics; + question: TSurveyCTAQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; 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 a5dd247aea..50478c8577 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 @@ -1,14 +1,13 @@ "use client"; import { md } from "@formbricks/lib/markdownIt"; -import type { ConsentQuestion } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; +import { TSurveyConsentQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Editor, Input, Label } from "@formbricks/ui"; import { useState } from "react"; interface ConsentQuestionFormProps { - localSurvey: Survey; - question: ConsentQuestion; + localSurvey: TSurveyWithAnalytics; + question: TSurveyConsentQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; isInValid: boolean; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/EditThankYouCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/EditThankYouCard.tsx index bea3cce96c..f7811092a6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/EditThankYouCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/EditThankYouCard.tsx @@ -1,13 +1,13 @@ "use client"; import { cn } from "@formbricks/lib/cn"; -import type { Survey } from "@formbricks/types/surveys"; import { Input, Label, Switch } from "@formbricks/ui"; import * as Collapsible from "@radix-ui/react-collapsible"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; interface EditThankYouCardProps { - localSurvey: Survey; - setLocalSurvey: (survey: Survey) => void; + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; setActiveQuestionId: (id: string | null) => void; activeQuestionId: string | null; } @@ -41,7 +41,7 @@ export default function EditThankYouCard({ return (
- +
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/HowToSendCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/HowToSendCard.tsx index ea7705e935..494b3c0010 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/HowToSendCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/HowToSendCard.tsx @@ -1,8 +1,5 @@ "use client"; - -import { useEnvironment } from "@/lib/environments/environments"; import { cn } from "@formbricks/lib/cn"; -import type { Survey } from "@formbricks/types/surveys"; import { Badge, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui"; import { CheckCircleIcon, @@ -15,17 +12,18 @@ import { import * as Collapsible from "@radix-ui/react-collapsible"; import Link from "next/link"; import { useEffect, useState } from "react"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; +import { TEnvironment } from "@formbricks/types/v1/environment"; interface HowToSendCardProps { - localSurvey: Survey; - setLocalSurvey: (survey: Survey) => void; - environmentId: string; + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; + environment: TEnvironment; } -export default function HowToSendCard({ localSurvey, setLocalSurvey, environmentId }: HowToSendCardProps) { +export default function HowToSendCard({ localSurvey, setLocalSurvey, environment }: HowToSendCardProps) { const [open, setOpen] = useState(localSurvey.type === "web" ? false : true); const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false); - const { environment } = useEnvironment(environmentId); useEffect(() => { if (environment && environment.widgetSetupCompleted) { @@ -150,7 +148,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment

Follow the{" "} set up guide diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/LogicEditor.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/LogicEditor.tsx index 05071ab7c1..386d43fb88 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/LogicEditor.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/LogicEditor.tsx @@ -1,5 +1,5 @@ -import { Logic, LogicCondition, Question, QuestionType } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; +import { LogicCondition, QuestionType } from "@formbricks/types/questions"; +import { TSurveyLogic, TSurveyQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Button, DropdownMenu, @@ -23,9 +23,9 @@ import { useMemo } from "react"; import { BsArrowDown, BsArrowReturnRight } from "react-icons/bs"; interface LogicEditorProps { - localSurvey: Survey; + localSurvey: TSurveyWithAnalytics; questionIdx: number; - question: Question; + question: TSurveyQuestion; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; } @@ -48,7 +48,7 @@ export default function LogicEditor({ if ("choices" in question) { return question.choices.map((choice) => choice.label); } else if ("range" in question) { - return Array.from({ length: question.range }, (_, i) => (i + 1).toString()); + return Array.from({ length: question.range ? question.range : 0 }, (_, i) => (i + 1).toString()); } else if (question.type === QuestionType.NPS) { return Array.from({ length: 11 }, (_, i) => (i + 0).toString()); } @@ -141,7 +141,7 @@ export default function LogicEditor({ }; const addLogic = () => { - const newLogic: Logic[] = !question.logic ? [] : question.logic; + const newLogic: TSurveyLogic[] = !question.logic ? [] : question.logic; newLogic.push({ condition: undefined, value: undefined, 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 2c1f28ed64..e8efe2bbd0 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 @@ -1,5 +1,3 @@ -import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; import { Button, Input, @@ -14,10 +12,11 @@ import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid"; import { createId } from "@paralleldrive/cuid2"; import { cn } from "@formbricks/lib/cn"; import { useEffect, useRef, useState } from "react"; +import { TSurveyMultipleChoiceMultiQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; interface OpenQuestionFormProps { - localSurvey: Survey; - question: MultipleChoiceMultiQuestion; + localSurvey: TSurveyWithAnalytics; + question: TSurveyMultipleChoiceMultiQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx index bf8b42ac61..6cfb2ae034 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx @@ -1,5 +1,3 @@ -import type { MultipleChoiceSingleQuestion } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; import { Button, Input, @@ -14,10 +12,11 @@ import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid"; import { createId } from "@paralleldrive/cuid2"; import { cn } from "@formbricks/lib/cn"; import { useEffect, useRef, useState } from "react"; +import { TSurveyMultipleChoiceSingleQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; interface OpenQuestionFormProps { - localSurvey: Survey; - question: MultipleChoiceSingleQuestion; + localSurvey: TSurveyWithAnalytics; + question: TSurveyMultipleChoiceSingleQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/NPSQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/NPSQuestionForm.tsx index 7661c5f397..296a4e9419 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/NPSQuestionForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/NPSQuestionForm.tsx @@ -1,12 +1,11 @@ -import type { NPSQuestion } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; +import { TSurveyNPSQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Button, Input, Label } from "@formbricks/ui"; import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid"; import { useState } from "react"; interface NPSQuestionFormProps { - localSurvey: Survey; - question: NPSQuestion; + localSurvey: TSurveyWithAnalytics; + question: TSurveyNPSQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; 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 9206c23fad..6c90f21795 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 @@ -1,12 +1,11 @@ -import type { OpenTextQuestion } from "@formbricks/types/questions"; -import { Survey } from "@formbricks/types/surveys"; +import { TSurveyOpenTextQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Button, Input, Label } from "@formbricks/ui"; import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid"; import { useState } from "react"; interface OpenQuestionFormProps { - localSurvey: Survey; - question: OpenTextQuestion; + localSurvey: TSurveyWithAnalytics; + question: TSurveyOpenTextQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; 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 954052ef66..143f6ebbff 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 @@ -4,7 +4,6 @@ import AdvancedSettings from "@/app/(app)/environments/[environmentId]/surveys/[ import { getQuestionTypeName } from "@/lib/questions"; import { cn } from "@formbricks/lib/cn"; import { QuestionType } from "@formbricks/types/questions"; -import type { Survey } from "@formbricks/types/surveys"; import { Input, Label, Switch } from "@formbricks/ui"; import { ChatBubbleBottomCenterTextIcon, @@ -28,9 +27,10 @@ import NPSQuestionForm from "./NPSQuestionForm"; import OpenQuestionForm from "./OpenQuestionForm"; import QuestionDropdown from "./QuestionMenu"; import RatingQuestionForm from "./RatingQuestionForm"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; interface QuestionCardProps { - localSurvey: Survey; + localSurvey: TSurveyWithAnalytics; questionIdx: number; moveQuestion: (questionIndex: number, up: boolean) => void; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; 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 221f51bbbb..08431725cf 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 @@ -1,6 +1,6 @@ "use client"; -import type { Survey } from "@formbricks/types/surveys"; +import React from "react"; import { createId } from "@paralleldrive/cuid2"; import { useMemo, useState } from "react"; import { DragDropContext } from "react-beautiful-dnd"; @@ -11,10 +11,11 @@ import QuestionCard from "./QuestionCard"; import { StrictModeDroppable } from "./StrictModeDroppable"; import { Question } from "@formbricks/types/questions"; import { validateQuestion } from "./Validation"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; interface QuestionsViewProps { - localSurvey: Survey; - setLocalSurvey: (survey: Survey) => void; + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; activeQuestionId: string | null; setActiveQuestionId: (questionId: string | null) => void; environmentId: string; @@ -40,7 +41,11 @@ export default function QuestionsView({ const [backButtonLabel, setbackButtonLabel] = useState(null); - const handleQuestionLogicChange = (survey: Survey, compareId: string, updatedId: string): Survey => { + const handleQuestionLogicChange = ( + survey: TSurveyWithAnalytics, + compareId: string, + updatedId: string + ): TSurveyWithAnalytics => { survey.questions.forEach((question) => { if (!question.logic) return; question.logic.forEach((rule) => { @@ -105,7 +110,7 @@ export default function QuestionsView({ const deleteQuestion = (questionIdx: number) => { const questionId = localSurvey.questions[questionIdx].id; - let updatedSurvey: Survey = JSON.parse(JSON.stringify(localSurvey)); + let updatedSurvey: TSurveyWithAnalytics = JSON.parse(JSON.stringify(localSurvey)); updatedSurvey.questions.splice(questionIdx, 1); updatedSurvey = handleQuestionLogicChange(updatedSurvey, questionId, "end"); 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 94a708de14..a02c43f6a3 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 @@ -1,14 +1,13 @@ -import type { RatingQuestion } from "@formbricks/types/questions"; -import type { Survey } from "@formbricks/types/surveys"; import { Button, Input, Label } from "@formbricks/ui"; import { FaceSmileIcon, HashtagIcon, StarIcon } from "@heroicons/react/24/outline"; import Dropdown from "./RatingTypeDropdown"; import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid"; import { useState } from "react"; +import { TSurveyRatingQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; interface RatingQuestionFormProps { - localSurvey: Survey; - question: RatingQuestion; + localSurvey: TSurveyWithAnalytics; + question: TSurveyRatingQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RecontactOptionsCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RecontactOptionsCard.tsx index beb6ad5b64..fc6876670e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RecontactOptionsCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RecontactOptionsCard.tsx @@ -1,7 +1,7 @@ "use client"; import { cn } from "@formbricks/lib/cn"; -import type { Survey } from "@formbricks/types/surveys"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { AdvancedOptionToggle, Badge, Input, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui"; import { CheckCircleIcon } from "@heroicons/react/24/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; @@ -33,8 +33,8 @@ const displayOptions: DisplayOption[] = [ ]; interface RecontactOptionsCardProps { - localSurvey: Survey; - setLocalSurvey: (survey: Survey) => void; + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; environmentId: string; } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx index cbb0d9694e..b2000f7733 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx @@ -1,6 +1,6 @@ "use client"; -import type { Survey } from "@formbricks/types/surveys"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { AdvancedOptionToggle, DatePicker, Input, Label } from "@formbricks/ui"; import { CheckCircleIcon } from "@heroicons/react/24/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; @@ -8,8 +8,8 @@ import { useEffect, useState } from "react"; import toast from "react-hot-toast"; interface ResponseOptionsCardProps { - localSurvey: Survey; - setLocalSurvey: (survey: Survey) => void; + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; } export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: ResponseOptionsCardProps) { @@ -17,7 +17,7 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res const autoComplete = localSurvey.autoComplete !== null; const [redirectToggle, setRedirectToggle] = useState(false); const [surveyCloseOnDateToggle, setSurveyCloseOnDateToggle] = useState(false); - + useState; const [redirectUrl, setRedirectUrl] = useState(""); const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false); const [verifyEmailToggle, setVerifyEmailToggle] = useState(false); @@ -95,6 +95,7 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res subheading?: string; }) => { const message = { + enabled: surveyCloseOnDateToggle, heading: heading ?? surveyClosedMessage.heading, subheading: subheading ?? surveyClosedMessage.subheading, }; @@ -149,16 +150,16 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res const handleCheckMark = () => { if (autoComplete) { - const updatedSurvey: Survey = { ...localSurvey, autoComplete: null }; + const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, autoComplete: null }; setLocalSurvey(updatedSurvey); } else { - const updatedSurvey: Survey = { ...localSurvey, autoComplete: 25 }; + const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, autoComplete: 25 }; setLocalSurvey(updatedSurvey); } }; const handleInputResponse = (e) => { - const updatedSurvey: Survey = { ...localSurvey, autoComplete: parseInt(e.target.value) }; + const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, autoComplete: parseInt(e.target.value) }; setLocalSurvey(updatedSurvey); }; @@ -168,11 +169,11 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res return; } - const inputResponses = localSurvey?._count?.responses || 0; + const inputResponses = localSurvey.analytics.numResponses || 0; if (parseInt(e.target.value) <= inputResponses) { toast.error( - `Response limit needs to exceed number of received responses (${localSurvey?._count?.responses}).` + `Response limit needs to exceed number of received responses (${localSurvey.analytics.numResponses}).` ); return; } @@ -211,7 +212,11 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res void; + environment: TEnvironment; + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; + actionClasses: TActionClass[]; + attributeClasses: TAttributeClass[]; } -export default function SettingsView({ environmentId, localSurvey, setLocalSurvey }: SettingsViewProps) { +export default function SettingsView({ + environment, + localSurvey, + setLocalSurvey, + actionClasses, + attributeClasses, +}: SettingsViewProps) { return (

- + @@ -37,7 +46,7 @@ export default function SettingsView({ environmentId, localSurvey, setLocalSurve
); 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 4a0badbb05..a5b910c85b 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 @@ -1,10 +1,6 @@ "use client"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useProduct } from "@/lib/products/products"; -import { useSurvey } from "@/lib/surveys/surveys"; -import type { Survey } from "@formbricks/types/surveys"; -import { ErrorComponent } from "@formbricks/ui"; +import React from "react"; import { useEffect, useState } from "react"; import PreviewSurvey from "../../PreviewSurvey"; import QuestionsAudienceTabs from "./QuestionsSettingsTabs"; @@ -12,24 +8,31 @@ import QuestionsView from "./QuestionsView"; import SettingsView from "./SettingsView"; import SurveyMenuBar from "./SurveyMenuBar"; import { TEnvironment } from "@formbricks/types/v1/environment"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; +import { TProduct } from "@formbricks/types/v1/product"; +import { TAttributeClass } from "@formbricks/types/v1/attributeClasses"; +import { TActionClass } from "@formbricks/types/v1/actionClasses"; +import { ErrorComponent } from "@formbricks/ui"; interface SurveyEditorProps { - environmentId: string; - surveyId: string; + survey: TSurveyWithAnalytics; + product: TProduct; environment: TEnvironment; + actionClasses: TActionClass[]; + attributeClasses: TAttributeClass[]; } export default function SurveyEditor({ - environmentId, - surveyId, + survey, + product, environment, + actionClasses, + attributeClasses, }: SurveyEditorProps): JSX.Element { const [activeView, setActiveView] = useState<"questions" | "settings">("questions"); const [activeQuestionId, setActiveQuestionId] = useState(null); - const [localSurvey, setLocalSurvey] = useState(); + const [localSurvey, setLocalSurvey] = useState(); const [invalidQuestions, setInvalidQuestions] = useState(null); - const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId, true); - const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); useEffect(() => { if (survey) { @@ -48,59 +51,58 @@ export default function SurveyEditor({ } }, [localSurvey?.type]); - if (isLoadingSurvey || isLoadingProduct || !localSurvey) { - return ; - } - - if (isErrorSurvey || isErrorProduct) { + if (!localSurvey) { return ; } return ( -
- -
-
- - {activeView === "questions" ? ( - +
+ +
+
+ + {activeView === "questions" ? ( + + ) : ( + + )} +
+
- + +
-
+ ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx index cc20c5195e..822ff1c9fe 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx @@ -1,11 +1,9 @@ "use client"; +import React from "react"; import AlertDialog from "@/components/shared/AlertDialog"; import DeleteDialog from "@/components/shared/DeleteDialog"; import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown"; -import { useProduct } from "@/lib/products/products"; -import { useSurveyMutation } from "@/lib/surveys/mutateSurveys"; -import { deleteSurvey } from "@/lib/surveys/surveys"; import type { Survey } from "@formbricks/types/surveys"; import { Button, Input } from "@formbricks/ui"; import { ArrowLeftIcon, Cog8ToothIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid"; @@ -14,35 +12,37 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { validateQuestion } from "./Validation"; +import { TSurvey, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; +import { deleteSurveyAction, surveyMutateAction } from "./actions"; +import { TProduct } from "@formbricks/types/v1/product"; import { TEnvironment } from "@formbricks/types/v1/environment"; interface SurveyMenuBarProps { - localSurvey: Survey; - survey: Survey; - setLocalSurvey: (survey: Survey) => void; - environmentId: string; + localSurvey: TSurveyWithAnalytics; + survey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; environment: TEnvironment; activeId: "questions" | "settings"; setActiveId: (id: "questions" | "settings") => void; setInvalidQuestions: (invalidQuestions: String[]) => void; + product: TProduct; } export default function SurveyMenuBar({ localSurvey, survey, - environmentId, environment, setLocalSurvey, activeId, setActiveId, setInvalidQuestions, + product, }: SurveyMenuBarProps) { const router = useRouter(); - const { triggerSurveyMutate, isMutatingSurvey } = useSurveyMutation(environmentId, localSurvey.id); const [audiencePrompt, setAudiencePrompt] = useState(true); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false); - const { product } = useProduct(environmentId); + const [isMutatingSurvey, setIsMutatingSurvey] = useState(false); let faultyQuestions: String[] = []; useEffect(() => { @@ -73,9 +73,10 @@ export default function SurveyMenuBar({ setLocalSurvey(updatedSurvey); }; - const deleteSurveyAction = async (survey) => { + const deleteSurvey = async (surveyId) => { try { - await deleteSurvey(environmentId, survey.id); + await deleteSurveyAction(surveyId); + router.refresh(); setDeleteDialogOpen(false); router.back(); } catch (error) { @@ -84,7 +85,10 @@ export default function SurveyMenuBar({ }; const handleBack = () => { - if (localSurvey.createdAt === localSurvey.updatedAt && localSurvey.status === "draft") { + const createdAt = new Date(localSurvey.createdAt).getTime(); + const updatedAt = new Date(localSurvey.updatedAt).getTime(); + + if (createdAt === updatedAt && localSurvey.status === "draft") { setDeleteDialogOpen(true); } else if (!isEqual(localSurvey, survey)) { setConfirmDialogOpen(true); @@ -121,21 +125,13 @@ export default function SurveyMenuBar({ return false; } - if ( - survey.redirectUrl && - !survey.redirectUrl.includes("https://") && - !survey.redirectUrl.includes("http://") - ) { - toast.error("Please enter a valid URL for redirecting respondents"); - 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 = { + const saveSurveyAction = async (shouldNavigateBack = false) => { + setIsMutatingSurvey(true); + // Create a copy of localSurvey with isDraft removed from every question + const strippedSurvey: TSurvey = { ...localSurvey, questions: localSurvey.questions.map((question) => { const { isDraft, ...rest } = question; @@ -147,28 +143,26 @@ export default function SurveyMenuBar({ return; } - triggerSurveyMutate({ ...strippedSurvey }) - .then(async (response) => { - if (!response?.ok) { - throw new Error(await response?.text()); - } - const updatedSurvey = await response.json(); - setLocalSurvey(updatedSurvey); - toast.success("Changes saved."); - if (shouldNavigateBack) { - router.back(); + try { + await surveyMutateAction({ ...strippedSurvey }); + router.refresh(); + setIsMutatingSurvey(false); + toast.success("Changes saved."); + if (shouldNavigateBack) { + router.back(); + } else { + if (localSurvey.status !== "draft") { + router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary`); } else { - if (localSurvey.status !== "draft") { - router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary`); - } else { - router.push(`/environments/${environmentId}/surveys`); - } + router.push(`/environments/${environment.id}/surveys`); } - }) - .catch((error) => { - console.log(error); - toast.error(`Error saving changes`); - }); + } + } catch (e) { + console.error(e); + setIsMutatingSurvey(false); + toast.error(`Error saving changes`); + return; + } }; return ( @@ -200,7 +194,7 @@ export default function SurveyMenuBar({ className="w-72 border-white hover:border-slate-200 " />
- {!!localSurvey?.responseRate && ( + {!!localSurvey.analytics.responseRate && (

@@ -212,7 +206,7 @@ export default function SurveyMenuBar({

@@ -239,16 +233,19 @@ export default function SurveyMenuBar({ disabled={ localSurvey.type === "web" && localSurvey.triggers && - (localSurvey.triggers[0] === "" || localSurvey.triggers.length === 0) + (localSurvey.triggers[0]?.id === "" || localSurvey.triggers.length === 0) } variant="darkCTA" loading={isMutatingSurvey} onClick={async () => { + setIsMutatingSurvey(true); if (!validateSurvey(localSurvey)) { return; } - await triggerSurveyMutate({ ...localSurvey, status: "inProgress" }); - router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary?success=true`); + await surveyMutateAction({ ...localSurvey, status: "inProgress" }); + router.refresh(); + setIsMutatingSurvey(false); + router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`); }}> Publish @@ -258,7 +255,7 @@ export default function SurveyMenuBar({ deleteWhat="Draft" open={isDeleteDialogOpen} setOpen={setDeleteDialogOpen} - onDelete={() => deleteSurveyAction(localSurvey)} + onDelete={() => deleteSurvey(localSurvey.id)} text="Do you want to delete this draft?" useSaveInsteadOfCancel={true} onSave={() => saveSurveyAction(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 index b11dc523e0..a2830b8f50 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts @@ -1,19 +1,19 @@ // extend this object in order to add more validation rules import { - MultipleChoiceMultiQuestion, - MultipleChoiceSingleQuestion, - Question, -} from "@formbricks/types/questions"; + TSurveyMultipleChoiceMultiQuestion, + TSurveyMultipleChoiceSingleQuestion, + TSurveyQuestion, +} from "@formbricks/types/v1/surveys"; const validationRules = { - multipleChoiceMulti: (question: MultipleChoiceMultiQuestion) => { + multipleChoiceMulti: (question: TSurveyMultipleChoiceMultiQuestion) => { return !question.choices.some((element) => element.label.trim() === ""); }, - multipleChoiceSingle: (question: MultipleChoiceSingleQuestion) => { + multipleChoiceSingle: (question: TSurveyMultipleChoiceSingleQuestion) => { return !question.choices.some((element) => element.label.trim() === ""); }, - defaultValidation: (question: Question) => { + defaultValidation: (question: TSurveyQuestion) => { return question.headline.trim() !== ""; }, }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx index 5147a5e7a9..391c2f4fc7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx @@ -1,10 +1,7 @@ "use client"; import AddNoCodeActionModal from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useEventClasses } from "@/lib/eventClasses/eventClasses"; import { cn } from "@formbricks/lib/cn"; -import type { Survey } from "@formbricks/types/surveys"; import { AdvancedOptionToggle, Badge, @@ -20,29 +17,51 @@ import { import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; import { useEffect, useState } from "react"; +import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; +import { TActionClass } from "@formbricks/types/v1/actionClasses"; interface WhenToSendCardProps { - localSurvey: Survey; - setLocalSurvey: (survey: Survey) => void; + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; environmentId: string; + actionClasses: TActionClass[]; } -export default function WhenToSendCard({ environmentId, localSurvey, setLocalSurvey }: WhenToSendCardProps) { +export default function WhenToSendCard({ + environmentId, + localSurvey, + setLocalSurvey, + actionClasses, +}: WhenToSendCardProps) { const [open, setOpen] = useState(localSurvey.type === "web" ? true : false); - const { eventClasses, isLoadingEventClasses, isErrorEventClasses } = useEventClasses(environmentId); const [isAddEventModalOpen, setAddEventModalOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(null); + const [actionClassArray, setActionClassArray] = useState(actionClasses); const autoClose = localSurvey.autoClose !== null; + let newTrigger = { + id: "", // Set the appropriate value for the id + createdAt: new Date(), + updatedAt: new Date(), + name: "", + type: "code" as const, // Set the appropriate value for the type + environmentId: "", + description: null, + noCodeConfig: null, + }; + const addTriggerEvent = () => { const updatedSurvey = { ...localSurvey }; - updatedSurvey.triggers = [...localSurvey.triggers, ""]; + updatedSurvey.triggers = [...localSurvey.triggers, newTrigger]; setLocalSurvey(updatedSurvey); }; - const setTriggerEvent = (idx: number, eventClassId: string) => { + const setTriggerEvent = (idx: number, actionClassId: string) => { const updatedSurvey = { ...localSurvey }; - updatedSurvey.triggers[idx] = eventClassId; + updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => { + return actionClass.id === actionClassId; + })!; setLocalSurvey(updatedSurvey); }; @@ -54,10 +73,10 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur const handleCheckMark = () => { if (autoClose) { - const updatedSurvey: Survey = { ...localSurvey, autoClose: null }; + const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, autoClose: null }; setLocalSurvey(updatedSurvey); } else { - const updatedSurvey: Survey = { ...localSurvey, autoClose: 10 }; + const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, autoClose: 10 }; setLocalSurvey(updatedSurvey); } }; @@ -67,15 +86,21 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur if (value < 1) value = 1; - const updatedSurvey: Survey = { ...localSurvey, autoClose: value }; + const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, autoClose: value }; setLocalSurvey(updatedSurvey); }; const handleTriggerDelay = (e: any) => { let value = parseInt(e.target.value); - const updatedSurvey: Survey = { ...localSurvey, delay: value }; + const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, delay: value }; setLocalSurvey(updatedSurvey); }; + useEffect(() => { + console.log(actionClassArray); + if (activeIndex !== null) { + setTriggerEvent(activeIndex, actionClassArray[actionClassArray.length - 1].id); + } + }, [actionClassArray]); useEffect(() => { if (localSurvey.type === "link") { @@ -90,14 +115,6 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur } }, []); - if (isLoadingEventClasses) { - return ; - } - - if (isErrorEventClasses) { - return
Error
; - } - return ( <>
- {!localSurvey.triggers || - localSurvey.triggers.length === 0 || - localSurvey.triggers[0] === "" ? ( + {!localSurvey.triggers || localSurvey.triggers.length === 0 || !localSurvey.triggers[0]?.id ? (

- {localSurvey.triggers?.map((triggerEventClassId, idx) => ( + {localSurvey.triggers?.map((triggerEventClass, idx) => (

{idx === 0 ? "When" : "or"}

+ onChange={(e) => { + e.preventDefault(); setAttributeFilter( idx, attributeFilter.attributeClassId, attributeFilter.condition, e.target.value - ) - } + ); + }} />
)} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts index 0e0f3a5ef6..656893783f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts @@ -2059,5 +2059,7 @@ export const minimalSurvey: TSurvey = { delay: 0, // No delay autoComplete: null, closeOnDate: null, - surveyClosedMessage: {}, + surveyClosedMessage: { + enabled: false, + }, }; diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx index 0874c50315..90ca90b507 100644 --- a/apps/web/app/s/[surveyId]/page.tsx +++ b/apps/web/app/s/[surveyId]/page.tsx @@ -21,7 +21,12 @@ export default async function LinkSurveyPage({ params, searchParams }) { const isPrefilledAnswerValid = prefillAnswer ? checkValidity(survey!.questions[0], prefillAnswer) : false; if (survey && survey.status !== "inProgress") { - return ; + return ( + + ); } // verify email: Check if the survey requires email verification diff --git a/apps/web/components/preview/TabOption.tsx b/apps/web/components/preview/TabOption.tsx index 3bff258736..e68f4efff9 100644 --- a/apps/web/components/preview/TabOption.tsx +++ b/apps/web/components/preview/TabOption.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react"; -export default function OptionButton({ +export default function TabOption({ active, icon, onClick, diff --git a/packages/lib/services/actionClass.ts b/packages/lib/services/actionClass.ts index d4c5c34518..5b3792de9c 100644 --- a/packages/lib/services/actionClass.ts +++ b/packages/lib/services/actionClass.ts @@ -7,7 +7,6 @@ import { validateInputs } from "../utils/validate"; import { ZId } from "@formbricks/types/v1/environment"; import { cache } from "react"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; - const select = { id: true, createdAt: true, diff --git a/packages/lib/services/attributeClass.ts b/packages/lib/services/attributeClass.ts index 619a5505f4..573b70c804 100644 --- a/packages/lib/services/attributeClass.ts +++ b/packages/lib/services/attributeClass.ts @@ -43,7 +43,6 @@ export const getAttributeClasses = cache(async (environmentId: string): Promise< throw new DatabaseError(`Database error when fetching attributeClasses for environment ${environmentId}`); } }); - export const updatetAttributeClass = async ( attributeClassId: string, data: Partial diff --git a/packages/lib/services/survey.ts b/packages/lib/services/survey.ts index d2c820252e..cdf4c76f0f 100644 --- a/packages/lib/services/survey.ts +++ b/packages/lib/services/survey.ts @@ -2,6 +2,7 @@ import { prisma } from "@formbricks/database"; import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors"; import { TSurvey, TSurveyWithAnalytics, ZSurvey, ZSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Prisma } from "@prisma/client"; +import { TSurveyAttributeFilter } from "@formbricks/types/v1/surveys"; import { cache } from "react"; import "server-only"; import { z } from "zod"; @@ -252,6 +253,187 @@ export const getSurveysWithAnalytics = cache( } ); +export async function updateSurvey(updatedSurvey: TSurvey): Promise { + const surveyId = updatedSurvey.id; + let data: any = {}; + let survey: Partial = { ...updatedSurvey }; + + if (updatedSurvey.triggers && updatedSurvey.triggers.length > 0) { + const modifiedTriggers = updatedSurvey.triggers.map((trigger) => { + if (typeof trigger === "object" && trigger.id) { + return trigger.id; + } else if (typeof trigger === "string" && trigger !== undefined) { + return trigger; + } + }); + + survey = { ...updatedSurvey, triggers: modifiedTriggers }; + } + + const currentTriggers = await prisma.surveyTrigger.findMany({ + where: { + surveyId, + }, + }); + const currentAttributeFilters = await prisma.surveyAttributeFilter.findMany({ + where: { + surveyId, + }, + }); + + delete survey.updatedAt; + // preventing issue with unknowingly updating analytics + delete survey.analytics; + + if (survey.type === "link") { + delete survey.triggers; + delete survey.recontactDays; + // converts JSON field with null value to JsonNull as JSON fields can't be set to null since prisma 3.0 + if (!survey.surveyClosedMessage) { + survey.surveyClosedMessage = null; + } + } + + if (survey.triggers) { + const newTriggers: string[] = []; + const removedTriggers: string[] = []; + // find added triggers + for (const eventClassId of survey.triggers) { + if (!eventClassId) { + continue; + } + if (currentTriggers.find((t) => t.eventClassId === eventClassId)) { + continue; + } else { + newTriggers.push(eventClassId); + } + } + // find removed triggers + for (const trigger of currentTriggers) { + if (survey.triggers.find((t: any) => t === trigger.eventClassId)) { + continue; + } else { + removedTriggers.push(trigger.eventClassId); + } + } + // create new triggers + if (newTriggers.length > 0) { + data.triggers = { + ...(data.triggers || []), + create: newTriggers.map((eventClassId) => ({ + eventClassId, + })), + }; + } + // delete removed triggers + if (removedTriggers.length > 0) { + data.triggers = { + ...(data.triggers || []), + deleteMany: { + eventClassId: { + in: removedTriggers, + }, + }, + }; + } + delete survey.triggers; + } + + const attributeFilters: TSurveyAttributeFilter[] = survey.attributeFilters; + if (attributeFilters) { + const newFilters: TSurveyAttributeFilter[] = []; + const removedFilterIds: string[] = []; + // find added attribute filters + for (const attributeFilter of attributeFilters) { + if (!attributeFilter.attributeClassId || !attributeFilter.condition || !attributeFilter.value) { + continue; + } + if ( + currentAttributeFilters.find( + (f) => + f.attributeClassId === attributeFilter.attributeClassId && + f.condition === attributeFilter.condition && + f.value === attributeFilter.value + ) + ) { + continue; + } else { + newFilters.push({ + attributeClassId: attributeFilter.attributeClassId, + condition: attributeFilter.condition, + value: attributeFilter.value, + }); + } + } + // find removed attribute filters + for (const attributeFilter of currentAttributeFilters) { + if ( + attributeFilters.find( + (f) => + f.attributeClassId === attributeFilter.attributeClassId && + f.condition === attributeFilter.condition && + f.value === attributeFilter.value + ) + ) { + continue; + } else { + removedFilterIds.push(attributeFilter.attributeClassId); + } + } + // create new attribute filters + if (newFilters.length > 0) { + data.attributeFilters = { + ...(data.attributeFilters || []), + create: newFilters.map((attributeFilter) => ({ + attributeClassId: attributeFilter.attributeClassId, + condition: attributeFilter.condition, + value: attributeFilter.value, + })), + }; + } + // delete removed triggers + if (removedFilterIds.length > 0) { + // delete all attribute filters that match the removed attribute classes + await Promise.all( + removedFilterIds.map(async (attributeClassId) => { + await prisma.surveyAttributeFilter.deleteMany({ + where: { + attributeClassId, + }, + }); + }) + ); + } + delete survey.attributeFilters; + } + + data = { + ...data, + ...survey, + }; + + try { + const prismaSurvey = await prisma.survey.update({ + where: { id: surveyId }, + data, + }); + + const modifiedSurvey: TSurvey = { + ...prismaSurvey, // Properties from prismaSurvey + triggers: updatedSurvey.triggers, // Include triggers from updatedSurvey + attributeFilters: updatedSurvey.attributeFilters, // Include attributeFilters from updatedSurvey + }; + + return modifiedSurvey; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } +} + export async function deleteSurvey(surveyId: string) { validateInputs([surveyId, ZId]); const deletedSurvey = await prisma.survey.delete({ diff --git a/packages/types/v1/surveys.ts b/packages/types/v1/surveys.ts index 224b542db9..10cdd85117 100644 --- a/packages/types/v1/surveys.ts +++ b/packages/types/v1/surveys.ts @@ -10,9 +10,11 @@ export const ZSurveyThankYouCard = z.object({ export const ZSurveyClosedMessage = z .object({ + enabled: z.boolean(), heading: z.optional(z.string()), subheading: z.optional(z.string()), }) + .nullable() .optional(); export const ZSurveyVerifyEmail = z @@ -135,6 +137,7 @@ const ZSurveyQuestionBase = z.object({ scale: z.enum(["number", "smiley", "star"]).optional(), range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]).optional(), logic: z.array(ZSurveyLogic).optional(), + isDraft: z.boolean().optional(), }); export const ZSurveyOpenTextQuestion = ZSurveyQuestionBase.extend({ @@ -223,12 +226,13 @@ export const ZSurveyQuestions = z.array(ZSurveyQuestion); export type TSurveyQuestions = z.infer; export const ZSurveyAttributeFilter = z.object({ - id: z.string().cuid2(), attributeClassId: z.string(), condition: z.enum(["equals", "notEquals"]), value: z.string(), }); +export type TSurveyAttributeFilter = z.infer; + export const ZSurvey = z.object({ id: z.string().cuid2(), createdAt: z.date(),