mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-08 23:59:38 -06:00
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 <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
ffef88731d
commit
993346b9ae
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<CTAQuestion>;
|
||||
questionSummary: QuestionSummary<TSurveyCTAQuestion>;
|
||||
}
|
||||
|
||||
interface ChoiceResult {
|
||||
|
||||
@@ -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<ConsentQuestion>;
|
||||
questionSummary: QuestionSummary<TSurveyConsentQuestion>;
|
||||
}
|
||||
|
||||
interface ChoiceResult {
|
||||
|
||||
@@ -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<MultipleChoiceMultiQuestion | MultipleChoiceSingleQuestion>;
|
||||
questionSummary: QuestionSummary<TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion>;
|
||||
environmentId: string;
|
||||
surveyType: string;
|
||||
}
|
||||
|
||||
@@ -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<NPSQuestion>;
|
||||
questionSummary: QuestionSummary<TSurveyNPSQuestion>;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
|
||||
@@ -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<OpenTextQuestion>;
|
||||
questionSummary: QuestionSummary<TSurveyOpenTextQuestion>;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RatingQuestion>;
|
||||
questionSummary: QuestionSummary<TSurveyRatingQuestion>;
|
||||
}
|
||||
|
||||
interface ChoiceResult {
|
||||
|
||||
@@ -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 (
|
||||
<OpenTextSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary as QuestionSummary<OpenTextQuestion>}
|
||||
questionSummary={questionSummary as QuestionSummary<TSurveyOpenTextQuestion>}
|
||||
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 (
|
||||
<NPSSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary as QuestionSummary<NPSQuestion>}
|
||||
questionSummary={questionSummary as QuestionSummary<TSurveyNPSQuestion>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -92,7 +93,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar
|
||||
return (
|
||||
<CTASummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary as QuestionSummary<CTAQuestion>}
|
||||
questionSummary={questionSummary as QuestionSummary<TSurveyCTAQuestion>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -100,7 +101,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar
|
||||
return (
|
||||
<RatingSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary as QuestionSummary<RatingQuestion>}
|
||||
questionSummary={questionSummary as QuestionSummary<TSurveyRatingQuestion>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -108,7 +109,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar
|
||||
return (
|
||||
<ConsentSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary as QuestionSummary<ConsentQuestion>}
|
||||
questionSummary={questionSummary as QuestionSummary<TSurveyConsentQuestion>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
"flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
@@ -86,7 +86,7 @@ export default function EditThankYouCard({
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-4">
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Headline</Label>
|
||||
|
||||
@@ -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
|
||||
<p className="text-xs font-normal">
|
||||
Follow the{" "}
|
||||
<Link
|
||||
href={`/environments/${environmentId}/settings/setup`}
|
||||
href={`/environments/${environment.id}/settings/setup`}
|
||||
className="underline hover:text-amber-900"
|
||||
target="_blank">
|
||||
set up guide
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string | null>("");
|
||||
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
|
||||
<Input
|
||||
autoFocus
|
||||
type="number"
|
||||
min={localSurvey?._count?.responses ? (localSurvey?._count?.responses + 1).toString() : "1"}
|
||||
min={
|
||||
localSurvey?.analytics?.numResponses
|
||||
? (localSurvey?.analytics?.numResponses + 1).toString()
|
||||
: "1"
|
||||
}
|
||||
id="autoCompleteResponses"
|
||||
value={localSurvey.autoComplete?.toString()}
|
||||
onChange={handleInputResponse}
|
||||
|
||||
@@ -1,35 +1,44 @@
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import HowToSendCard from "./HowToSendCard";
|
||||
import RecontactOptionsCard from "./RecontactOptionsCard";
|
||||
import ResponseOptionsCard from "./ResponseOptionsCard";
|
||||
import WhenToSendCard from "./WhenToSendCard";
|
||||
import WhoToSendCard from "./WhoToSendCard";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
|
||||
|
||||
interface SettingsViewProps {
|
||||
environmentId: string;
|
||||
localSurvey: Survey;
|
||||
setLocalSurvey: (survey: Survey) => 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 (
|
||||
<div className="mt-12 space-y-3 p-5">
|
||||
<HowToSendCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<HowToSendCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} environment={environment} />
|
||||
|
||||
<WhoToSendCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<WhenToSendCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
actionClasses={actionClasses}
|
||||
/>
|
||||
|
||||
<ResponseOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
@@ -37,7 +46,7 @@ export default function SettingsView({ environmentId, localSurvey, setLocalSurve
|
||||
<RecontactOptionsCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [localSurvey, setLocalSurvey] = useState<Survey | null>();
|
||||
const [localSurvey, setLocalSurvey] = useState<TSurveyWithAnalytics | null>();
|
||||
const [invalidQuestions, setInvalidQuestions] = useState<String[] | null>(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 <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorSurvey || isErrorProduct) {
|
||||
if (!localSurvey) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<SurveyMenuBar
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
localSurvey={localSurvey}
|
||||
survey={survey}
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
activeId={activeView}
|
||||
setActiveId={setActiveView}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
/>
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none">
|
||||
<QuestionsAudienceTabs activeId={activeView} setActiveId={setActiveView} />
|
||||
{activeView === "questions" ? (
|
||||
<QuestionsView
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeQuestionId={activeQuestionId}
|
||||
<>
|
||||
<div className="flex h-full flex-col">
|
||||
<SurveyMenuBar
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
localSurvey={localSurvey}
|
||||
survey={survey}
|
||||
environment={environment}
|
||||
activeId={activeView}
|
||||
setActiveId={setActiveView}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
product={product}
|
||||
/>
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none">
|
||||
<QuestionsAudienceTabs activeId={activeView} setActiveId={setActiveView} />
|
||||
{activeView === "questions" ? (
|
||||
<QuestionsView
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
environmentId={environment.id}
|
||||
invalidQuestions={invalidQuestions}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
/>
|
||||
) : (
|
||||
<SettingsView
|
||||
environment={environment}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 py-6 md:flex md:flex-col">
|
||||
<PreviewSurvey
|
||||
survey={localSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
environmentId={environmentId}
|
||||
invalidQuestions={invalidQuestions}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
activeQuestionId={activeQuestionId}
|
||||
product={product}
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
|
||||
/>
|
||||
) : (
|
||||
<SettingsView
|
||||
environmentId={environmentId}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 py-6 md:flex md:flex-col">
|
||||
<PreviewSurvey
|
||||
survey={localSurvey}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
environmentId={environmentId}
|
||||
product={product}
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
|
||||
/>
|
||||
</aside>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 "
|
||||
/>
|
||||
</div>
|
||||
{!!localSurvey?.responseRate && (
|
||||
{!!localSurvey.analytics.responseRate && (
|
||||
<div className="mx-auto flex items-center rounded-full border border-amber-200 bg-amber-100 p-2 text-amber-700 shadow-sm">
|
||||
<ExclamationTriangleIcon className=" h-5 w-5 text-amber-400" />
|
||||
<p className="max-w-[90%] pl-1 text-xs lg:text-sm">
|
||||
@@ -212,7 +206,7 @@ export default function SurveyMenuBar({
|
||||
<div className="mr-4 flex items-center">
|
||||
<SurveyStatusDropdown
|
||||
surveyId={localSurvey.id}
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
updateLocalSurveyStatus={updateLocalSurveyStatus}
|
||||
/>
|
||||
</div>
|
||||
@@ -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
|
||||
</Button>
|
||||
@@ -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)}
|
||||
|
||||
@@ -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() !== "";
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
const [actionClassArray, setActionClassArray] = useState<TActionClass[]>(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 <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorEventClasses) {
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapsible.Root
|
||||
@@ -118,9 +135,7 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
)}>
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
{!localSurvey.triggers ||
|
||||
localSurvey.triggers.length === 0 ||
|
||||
localSurvey.triggers[0] === "" ? (
|
||||
{!localSurvey.triggers || localSurvey.triggers.length === 0 || !localSurvey.triggers[0]?.id ? (
|
||||
<div
|
||||
className={cn(
|
||||
localSurvey.type !== "link"
|
||||
@@ -152,13 +167,13 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="">
|
||||
<hr className="py-1 text-slate-600" />
|
||||
{localSurvey.triggers?.map((triggerEventClassId, idx) => (
|
||||
{localSurvey.triggers?.map((triggerEventClass, idx) => (
|
||||
<div className="mt-2" key={idx}>
|
||||
<div className="inline-flex items-center">
|
||||
<p className="mr-2 w-14 text-right text-sm">{idx === 0 ? "When" : "or"}</p>
|
||||
<Select
|
||||
value={triggerEventClassId}
|
||||
onValueChange={(eventClassId) => setTriggerEvent(idx, eventClassId)}>
|
||||
value={triggerEventClass.id}
|
||||
onValueChange={(actionClassId) => setTriggerEvent(idx, actionClassId)}>
|
||||
<SelectTrigger className="w-[240px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -168,14 +183,18 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
value="none"
|
||||
onClick={() => {
|
||||
setAddEventModalOpen(true);
|
||||
setActiveIndex(idx);
|
||||
}}>
|
||||
<PlusIcon className="mr-1 h-5 w-5" />
|
||||
Add Action
|
||||
</button>
|
||||
<SelectSeparator />
|
||||
{eventClasses.map((eventClass) => (
|
||||
<SelectItem value={eventClass.id} key={eventClass.id} title={eventClass.description}>
|
||||
{eventClass.name}
|
||||
{actionClassArray.map((actionClass) => (
|
||||
<SelectItem
|
||||
value={actionClass.id}
|
||||
key={actionClass.id}
|
||||
title={actionClass.description ? actionClass.description : ""}>
|
||||
{actionClass.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -249,6 +268,7 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
environmentId={environmentId}
|
||||
open={isAddEventModalOpen}
|
||||
setOpen={setAddEventModalOpen}
|
||||
setActionClassArray={setActionClassArray}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useAttributeClasses } from "@/lib/attributeClasses/attributeClasses";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -17,6 +14,8 @@ import {
|
||||
import { CheckCircleIcon, FunnelIcon, PlusIcon, TrashIcon, UserGroupIcon } 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 { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
|
||||
|
||||
const filterConditions = [
|
||||
{ id: "equals", name: "equals" },
|
||||
@@ -24,23 +23,15 @@ const filterConditions = [
|
||||
];
|
||||
|
||||
interface WhoToSendCardProps {
|
||||
localSurvey: Survey;
|
||||
setLocalSurvey: (survey: Survey) => void;
|
||||
localSurvey: TSurveyWithAnalytics;
|
||||
setLocalSurvey: (survey: TSurveyWithAnalytics) => void;
|
||||
environmentId: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export default function WhoToSendCard({ environmentId, localSurvey, setLocalSurvey }: WhoToSendCardProps) {
|
||||
export default function WhoToSendCard({ localSurvey, setLocalSurvey, attributeClasses }: WhoToSendCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { attributeClasses, isLoadingAttributeClasses, isErrorAttributeClasses } =
|
||||
useAttributeClasses(environmentId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingAttributeClasses) {
|
||||
if (localSurvey.attributeFilters?.length > 0) {
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
}, [isLoadingAttributeClasses]);
|
||||
const condition = filterConditions[0].id === "equals" ? "equals" : "notEquals";
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.type === "link") {
|
||||
@@ -52,14 +43,18 @@ export default function WhoToSendCard({ environmentId, localSurvey, setLocalSurv
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
updatedSurvey.attributeFilters = [
|
||||
...localSurvey.attributeFilters,
|
||||
{ attributeClassId: "", condition: filterConditions[0].id, value: "" },
|
||||
{ attributeClassId: "", condition: condition, value: "" },
|
||||
];
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
const setAttributeFilter = (idx: number, attributeClassId: string, condition: string, value: string) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
updatedSurvey.attributeFilters[idx] = { attributeClassId, condition, value };
|
||||
updatedSurvey.attributeFilters[idx] = {
|
||||
attributeClassId,
|
||||
condition: condition === "equals" ? "equals" : "notEquals",
|
||||
value,
|
||||
};
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
@@ -72,14 +67,6 @@ export default function WhoToSendCard({ environmentId, localSurvey, setLocalSurv
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
if (isLoadingAttributeClasses) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorAttributeClasses) {
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapsible.Root
|
||||
@@ -187,14 +174,15 @@ export default function WhoToSendCard({ environmentId, localSurvey, setLocalSurv
|
||||
</Select>
|
||||
<Input
|
||||
value={attributeFilter.value}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setAttributeFilter(
|
||||
idx,
|
||||
attributeFilter.attributeClassId,
|
||||
attributeFilter.condition,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => removeAttributeFilter(idx)}>
|
||||
<TrashIcon className="h-4 w-4 text-slate-400" />
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"use server";
|
||||
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { deleteSurvey, updateSurvey } from "@formbricks/lib/services/survey";
|
||||
|
||||
export async function surveyMutateAction(survey: TSurvey): Promise<TSurvey> {
|
||||
return await updateSurvey(survey);
|
||||
}
|
||||
|
||||
export async function deleteSurveyAction(surveyId: string) {
|
||||
await deleteSurvey(surveyId);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-between p-6">
|
||||
{/* Top Part - Loading Navbar */}
|
||||
<div className="flex h-[10vh] w-full animate-pulse rounded-lg bg-gray-200 font-medium text-slate-900"></div>
|
||||
|
||||
{/* Bottom Part - Divided into Left and Right */}
|
||||
<div className="mt-4 flex h-[85%] w-full flex-row">
|
||||
{/* Left Part - 7 Horizontal Bars */}
|
||||
<div className="flex h-full w-1/2 flex-col justify-between space-y-2">
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
|
||||
</div>
|
||||
|
||||
{/* Right Part - Simple Box */}
|
||||
<div className="ml-4 flex h-full w-1/2 flex-col">
|
||||
<div className="ph-no-capture h-full animate-pulse rounded-lg bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,35 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
import React from "react";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import SurveyEditor from "./SurveyEditor";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getActionClasses } from "@formbricks/lib/services/actionClass";
|
||||
import { getAttributeClasses } from "@formbricks/lib/services/attributeClass";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
|
||||
export default async function SurveysEditPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
const [survey, product, environment, actionClasses, attributeClasses] = await Promise.all([
|
||||
getSurveyWithAnalytics(params.surveyId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getActionClasses(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
if (!survey || !environment || !actionClasses || !attributeClasses || !product) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SurveyEditor environmentId={params.environmentId} surveyId={params.surveyId} environment={environment} />
|
||||
<>
|
||||
<SurveyEditor
|
||||
survey={survey}
|
||||
product={product}
|
||||
environment={environment}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ export default function TemplateContainerWithPreview({
|
||||
product={product}
|
||||
environment={environment}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2059,5 +2059,7 @@ export const minimalSurvey: TSurvey = {
|
||||
delay: 0, // No delay
|
||||
autoComplete: null,
|
||||
closeOnDate: null,
|
||||
surveyClosedMessage: {},
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 <SurveyInactive status={survey.status} surveyClosedMessage={survey.surveyClosedMessage} />;
|
||||
return (
|
||||
<SurveyInactive
|
||||
status={survey.status}
|
||||
surveyClosedMessage={survey.surveyClosedMessage ? survey.surveyClosedMessage : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// verify email: Check if the survey requires email verification
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function OptionButton({
|
||||
export default function TabOption({
|
||||
active,
|
||||
icon,
|
||||
onClick,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<TAttributeClassUpdateInput>
|
||||
|
||||
@@ -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<TSurvey> {
|
||||
const surveyId = updatedSurvey.id;
|
||||
let data: any = {};
|
||||
let survey: Partial<any> = { ...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({
|
||||
|
||||
@@ -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<typeof ZSurveyQuestions>;
|
||||
|
||||
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<typeof ZSurveyAttributeFilter>;
|
||||
|
||||
export const ZSurvey = z.object({
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.date(),
|
||||
|
||||
Reference in New Issue
Block a user