mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-12 09:39:39 -06:00
fix: survey jumping to 3rd question with prefilling (#2421)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
committed by
GitHub
parent
1ddd1cfc3d
commit
7f438afc30
@@ -18,6 +18,8 @@ import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import { updateProductAction } from "../actions";
|
||||
|
||||
let setQuestionId = (_: string) => {};
|
||||
|
||||
type ThemeStylingProps = {
|
||||
product: TProduct;
|
||||
environmentId: string;
|
||||
@@ -47,12 +49,10 @@ export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingPro
|
||||
}));
|
||||
};
|
||||
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||
|
||||
const [styledPreviewSurvey, setStyledPreviewSurvey] = useState<TSurvey>(PREVIEW_SURVEY);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveQuestionId(PREVIEW_SURVEY.questions[0].id);
|
||||
setQuestionId(PREVIEW_SURVEY.questions[0].id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -234,8 +234,7 @@ export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingPro
|
||||
<div className="relative w-1/2 rounded-lg bg-slate-100 pt-4">
|
||||
<div className="sticky top-4 mb-4 h-full max-h-[600px]">
|
||||
<ThemeStylingPreviewSurvey
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
setQuestionId={setQuestionId}
|
||||
survey={styledPreviewSurvey as TSurvey}
|
||||
product={localProduct}
|
||||
previewType={previewSurveyType}
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
|
||||
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
|
||||
import { Variants, motion } from "framer-motion";
|
||||
import { Repeat2 } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import type { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ClientLogo } from "@formbricks/ui/ClientLogo";
|
||||
import { ResetProgressButton } from "@formbricks/ui/ResetProgressButton";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
|
||||
interface ThemeStylingPreviewSurveyProps {
|
||||
survey: TSurvey;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId?: string | null;
|
||||
setQuestionId: (_: string) => void;
|
||||
product: TProduct;
|
||||
previewType: "link" | "web";
|
||||
setPreviewType: (type: "link" | "web") => void;
|
||||
@@ -49,12 +47,11 @@ const previewParentContainerVariant: Variants = {
|
||||
};
|
||||
|
||||
export const ThemeStylingPreviewSurvey = ({
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
survey,
|
||||
product,
|
||||
previewType,
|
||||
setPreviewType,
|
||||
setQuestionId,
|
||||
}: ThemeStylingPreviewSurveyProps) => {
|
||||
const [isFullScreenPreview] = useState(false);
|
||||
const [previewPosition] = useState("relative");
|
||||
@@ -109,7 +106,7 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
const highlightBorderColor = product.styling.highlightBorderColor?.light;
|
||||
|
||||
function resetQuestionProgress() {
|
||||
setActiveQuestionId(survey?.questions[0]?.id);
|
||||
setQuestionId(survey?.questions[0]?.id);
|
||||
}
|
||||
|
||||
const onFileUpload = async (file: File) => file.name;
|
||||
@@ -143,7 +140,7 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
<p>{previewType === "web" ? "Your web app" : "Preview"}</p>
|
||||
|
||||
<div className="flex items-center">
|
||||
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
||||
<ResetProgressButton onClick={resetQuestionProgress} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,14 +157,15 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
borderRadius={product.styling.roundness ?? 8}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={product.styling}
|
||||
isCardBorderVisible={!highlightBorderColor}
|
||||
languageCode="default"
|
||||
getSetQuestionId={(f: (value: string) => void) => {
|
||||
setQuestionId = f;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
@@ -181,14 +179,15 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
className={`${product.logo?.url && !product.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md rounded-lg p-4`}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
responseCount={42}
|
||||
styling={product.styling}
|
||||
languageCode="default"
|
||||
getSetQuestionId={(f: (value: string) => void) => {
|
||||
setQuestionId = f;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</MediaBackground>
|
||||
@@ -213,15 +212,3 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResetProgressButton = ({ resetQuestionProgress }: { resetQuestionProgress: () => void }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="minimal"
|
||||
className="py-0.2 mr-2 bg-white px-2 font-sans text-sm text-slate-500"
|
||||
onClick={resetQuestionProgress}>
|
||||
Restart
|
||||
<Repeat2 className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useMemo } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyEditorTabs } from "@formbricks/types/surveys";
|
||||
|
||||
type Tab = {
|
||||
interface Tab {
|
||||
id: TSurveyEditorTabs;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
};
|
||||
}
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
@@ -34,11 +34,11 @@ interface QuestionsAudienceTabsProps {
|
||||
isStylingTabVisible?: boolean;
|
||||
}
|
||||
|
||||
export default function QuestionsAudienceTabs({
|
||||
export const QuestionsAudienceTabs = ({
|
||||
activeId,
|
||||
setActiveId,
|
||||
isStylingTabVisible,
|
||||
}: QuestionsAudienceTabsProps) {
|
||||
}: QuestionsAudienceTabsProps) => {
|
||||
const tabsComputed = useMemo(() => {
|
||||
if (isStylingTabVisible) {
|
||||
return tabs;
|
||||
@@ -68,4 +68,4 @@ export default function QuestionsAudienceTabs({
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ interface QuestionsViewProps {
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default function QuestionsView({
|
||||
export const QuestionsView = ({
|
||||
activeQuestionId,
|
||||
setActiveQuestionId,
|
||||
localSurvey,
|
||||
@@ -49,7 +49,7 @@ export default function QuestionsView({
|
||||
selectedLanguageCode,
|
||||
isMultiLanguageAllowed,
|
||||
isFormbricksCloud,
|
||||
}: QuestionsViewProps) {
|
||||
}: QuestionsViewProps) => {
|
||||
const internalQuestionIdMap = useMemo(() => {
|
||||
return localSurvey.questions.reduce((acc, question) => {
|
||||
acc[question.id] = createId();
|
||||
@@ -362,4 +362,4 @@ export default function QuestionsView({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ interface SettingsViewProps {
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default function SettingsView({
|
||||
export const SettingsView = ({
|
||||
environment,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
@@ -38,7 +38,7 @@ export default function SettingsView({
|
||||
membershipRole,
|
||||
isUserTargetingAllowed = false,
|
||||
isFormbricksCloud,
|
||||
}: SettingsViewProps) {
|
||||
}: SettingsViewProps) => {
|
||||
return (
|
||||
<div className="mt-12 space-y-3 p-5">
|
||||
<HowToSendCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} environment={environment} />
|
||||
@@ -98,4 +98,4 @@ export default function SettingsView({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
import { refetchProduct } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton";
|
||||
import { QuestionsAudienceTabs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs";
|
||||
import { QuestionsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView";
|
||||
import { SettingsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView";
|
||||
import { StylingView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingView";
|
||||
import { SurveyMenuBar } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar";
|
||||
import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/actions";
|
||||
@@ -16,12 +21,6 @@ import { TProduct } from "@formbricks/types/product";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys";
|
||||
|
||||
import PreviewSurvey from "../../../components/PreviewSurvey";
|
||||
import QuestionsAudienceTabs from "./QuestionsStylingSettingsTabs";
|
||||
import QuestionsView from "./QuestionsView";
|
||||
import SettingsView from "./SettingsView";
|
||||
import SurveyMenuBar from "./SurveyMenuBar";
|
||||
|
||||
interface SurveyEditorProps {
|
||||
survey: TSurvey;
|
||||
product: TProduct;
|
||||
@@ -232,8 +231,7 @@ export default function SurveyEditor({
|
||||
<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}
|
||||
activeQuestionId={activeQuestionId}
|
||||
questionId={activeQuestionId}
|
||||
product={localProduct}
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
|
||||
|
||||
@@ -45,7 +45,7 @@ interface SurveyMenuBarProps {
|
||||
setSelectedLanguageCode: (selectedLanguage: string) => void;
|
||||
}
|
||||
|
||||
export default function SurveyMenuBar({
|
||||
export const SurveyMenuBar = ({
|
||||
localSurvey,
|
||||
survey,
|
||||
environment,
|
||||
@@ -57,7 +57,7 @@ export default function SurveyMenuBar({
|
||||
responseCount,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: SurveyMenuBarProps) {
|
||||
}: SurveyMenuBarProps) => {
|
||||
const router = useRouter();
|
||||
const [audiencePrompt, setAudiencePrompt] = useState(true);
|
||||
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
@@ -497,4 +497,4 @@ export default function SurveyMenuBar({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,24 +4,23 @@ import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/M
|
||||
import TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption";
|
||||
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
|
||||
import { Variants, motion } from "framer-motion";
|
||||
import { ExpandIcon, MonitorIcon, RefreshCcwIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ExpandIcon, MonitorIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import type { TProduct } from "@formbricks/types/product";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ClientLogo } from "@formbricks/ui/ClientLogo";
|
||||
import { ResetProgressButton } from "@formbricks/ui/ResetProgressButton";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
|
||||
type TPreviewType = "modal" | "fullwidth" | "email";
|
||||
|
||||
interface PreviewSurveyProps {
|
||||
survey: TSurvey;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId?: string | null;
|
||||
questionId?: string | null;
|
||||
previewType?: TPreviewType;
|
||||
product: TProduct;
|
||||
environment: TEnvironment;
|
||||
@@ -58,16 +57,17 @@ const previewParentContainerVariant: Variants = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function PreviewSurvey({
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
let setQuestionId = (_: string) => {};
|
||||
|
||||
export const PreviewSurvey = ({
|
||||
questionId,
|
||||
survey,
|
||||
previewType,
|
||||
product,
|
||||
environment,
|
||||
languageCode,
|
||||
onFileUpload,
|
||||
}: PreviewSurveyProps) {
|
||||
}: PreviewSurveyProps) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
||||
const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
|
||||
@@ -76,7 +76,6 @@ export default function PreviewSurvey({
|
||||
const ContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const [shrink, setShrink] = useState(false);
|
||||
const { productOverwrites } = survey || {};
|
||||
|
||||
const previewScreenVariants: Variants = {
|
||||
expanded: {
|
||||
right: "5%",
|
||||
@@ -140,18 +139,31 @@ export default function PreviewSurvey({
|
||||
return product.styling;
|
||||
}, [product.styling, survey.styling]);
|
||||
|
||||
const updateQuestionId = useCallback(
|
||||
(newQuestionId: string) => {
|
||||
if (!newQuestionId || newQuestionId === "hidden" || newQuestionId === "multiLanguage") return;
|
||||
if (newQuestionId === "start" && !survey.welcomeCard.enabled) return;
|
||||
setQuestionId(newQuestionId);
|
||||
},
|
||||
[survey.welcomeCard.enabled]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (questionId) {
|
||||
updateQuestionId(questionId);
|
||||
}
|
||||
}, [questionId, updateQuestionId]);
|
||||
|
||||
const onFinished = () => {
|
||||
// close modal if there are no questions left
|
||||
if (survey.type === "web" && !survey.thankYouCard.enabled) {
|
||||
if (activeQuestionId === "end") {
|
||||
setIsModalOpen(false);
|
||||
setTimeout(() => {
|
||||
setActiveQuestionId(survey.questions[0]?.id);
|
||||
setIsModalOpen(true);
|
||||
}, 500);
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setTimeout(() => {
|
||||
setQuestionId(survey.questions[0]?.id);
|
||||
setIsModalOpen(true);
|
||||
}, 500);
|
||||
}
|
||||
}, [activeQuestionId, survey.type, survey, setActiveQuestionId]);
|
||||
};
|
||||
|
||||
// this useEffect is fo refreshing the survey preview only if user is switching between templates on survey templates page and hence we are checking for survey.id === "someUniqeId1" which is a common Id for all templates
|
||||
useEffect(() => {
|
||||
@@ -169,7 +181,7 @@ export default function PreviewSurvey({
|
||||
setPreviewMode(storePreviewMode);
|
||||
}, 10);
|
||||
|
||||
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
||||
setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -190,7 +202,7 @@ export default function PreviewSurvey({
|
||||
if (!previewType) {
|
||||
previewType = widgetSetupCompleted ? "modal" : "fullwidth";
|
||||
|
||||
if (!activeQuestionId) {
|
||||
if (!questionId) {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -219,7 +231,7 @@ export default function PreviewSurvey({
|
||||
Preview
|
||||
</p>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
||||
<ResetProgressButton onClick={resetQuestionProgress} />
|
||||
</div>
|
||||
<MediaBackground survey={survey} product={product} ContentRef={ContentRef} isMobilePreview>
|
||||
{previewType === "modal" ? (
|
||||
@@ -234,15 +246,17 @@ export default function PreviewSurvey({
|
||||
background={styling?.cardBackgroundColor?.light}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={styling}
|
||||
isCardBorderVisible={!styling.highlightBorderColor?.light}
|
||||
onClose={handlePreviewModalClose}
|
||||
getSetQuestionId={(f: (value: string) => void) => {
|
||||
setQuestionId = f;
|
||||
}}
|
||||
onFinished={onFinished}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
@@ -255,13 +269,14 @@ export default function PreviewSurvey({
|
||||
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
styling={styling}
|
||||
getSetQuestionId={(f: (value: string) => void) => {
|
||||
setQuestionId = f;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -300,7 +315,7 @@ export default function PreviewSurvey({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
||||
<ResetProgressButton onClick={resetQuestionProgress} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,15 +332,17 @@ export default function PreviewSurvey({
|
||||
background={styling.cardBackgroundColor?.light}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={styling}
|
||||
isCardBorderVisible={!styling.highlightBorderColor?.light}
|
||||
onClose={handlePreviewModalClose}
|
||||
getSetQuestionId={(f: (value: string) => void) => {
|
||||
setQuestionId = f;
|
||||
}}
|
||||
onFinished={onFinished}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
@@ -338,14 +355,15 @@ export default function PreviewSurvey({
|
||||
<div className="z-0 w-full max-w-md rounded-lg border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
styling={styling}
|
||||
getSetQuestionId={(f: (value: string) => void) => {
|
||||
setQuestionId = f;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</MediaBackground>
|
||||
@@ -369,16 +387,4 @@ export default function PreviewSurvey({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResetProgressButton({ resetQuestionProgress }) {
|
||||
return (
|
||||
<Button
|
||||
variant="minimal"
|
||||
className="py-0.2 mr-2 bg-white px-2 font-sans text-sm text-slate-500"
|
||||
onClick={resetQuestionProgress}>
|
||||
Restart
|
||||
<RefreshCcwIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import TemplateList from "@/app/(app)/environments/[environmentId]/surveys/templates/TemplateList";
|
||||
import { TemplateList } from "@/app/(app)/environments/[environmentId]/surveys/templates/TemplateList";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
|
||||
import { TemplateList } from "@/app/(app)/environments/[environmentId]/surveys/templates/TemplateList";
|
||||
import { replacePresetPlaceholders } from "@/app/lib/templates";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
@@ -9,8 +11,6 @@ import type { TTemplate } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { SearchBox } from "@formbricks/ui/SearchBox";
|
||||
|
||||
import PreviewSurvey from "../components/PreviewSurvey";
|
||||
import TemplateList from "./TemplateList";
|
||||
import { minimalSurvey, templates } from "./templates";
|
||||
|
||||
type TemplateContainerWithPreviewProps = {
|
||||
@@ -73,10 +73,9 @@ export default function TemplateContainerWithPreview({
|
||||
<div className="my-6 flex h-[90%] w-full flex-col items-center justify-center">
|
||||
<PreviewSurvey
|
||||
survey={{ ...minimalSurvey, ...activeTemplate.preset }}
|
||||
activeQuestionId={activeQuestionId}
|
||||
questionId={activeQuestionId}
|
||||
product={product}
|
||||
environment={environment}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
languageCode={"default"}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
|
||||
@@ -17,25 +17,26 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formb
|
||||
import { createSurveyAction } from "../actions";
|
||||
import { customSurvey, templates, testTemplate } from "./templates";
|
||||
|
||||
type TemplateList = {
|
||||
interface TemplateList {
|
||||
environmentId: string;
|
||||
user: TUser;
|
||||
onTemplateClick: (template: TTemplate) => void;
|
||||
environment: TEnvironment;
|
||||
product: TProduct;
|
||||
templateSearch?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ALL_CATEGORY_NAME = "All";
|
||||
const RECOMMENDED_CATEGORY_NAME = "For you";
|
||||
export default function TemplateList({
|
||||
|
||||
export const TemplateList = ({
|
||||
environmentId,
|
||||
user,
|
||||
onTemplateClick,
|
||||
product,
|
||||
environment,
|
||||
templateSearch,
|
||||
}: TemplateList) {
|
||||
}: TemplateList) => {
|
||||
const router = useRouter();
|
||||
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -210,4 +211,4 @@ export default function TemplateList({
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
|
||||
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
|
||||
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
import { RefreshCcwIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
@@ -16,10 +15,12 @@ import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { ClientLogo } from "@formbricks/ui/ClientLogo";
|
||||
import { ContentWrapper } from "@formbricks/ui/ContentWrapper";
|
||||
import { ResetProgressButton } from "@formbricks/ui/ResetProgressButton";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
|
||||
let setIsError = (_: boolean) => {};
|
||||
let setIsResponseSendingFinished = (_: boolean) => {};
|
||||
let setQuestionId = (_: string) => {};
|
||||
|
||||
interface LinkSurveyProps {
|
||||
survey: TSurvey;
|
||||
@@ -76,10 +77,6 @@ export default function LinkSurvey({
|
||||
|
||||
// pass in the responseId if the survey is a single use survey, ensures survey state is updated with the responseId
|
||||
const [surveyState, setSurveyState] = useState(new SurveyState(survey.id, singleUseId, responseId, userId));
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string>(
|
||||
startAt && isStartAtValid ? startAt : survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id
|
||||
);
|
||||
|
||||
const prefillResponseData: TResponseData | undefined = prefillAnswer
|
||||
? getPrefillResponseData(survey.questions[0], survey, prefillAnswer, languageCode)
|
||||
: undefined;
|
||||
@@ -118,6 +115,7 @@ export default function LinkSurvey({
|
||||
if (window.self === window.top) {
|
||||
setAutofocus(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const hiddenFieldsRecord = useMemo<Record<string, string | number | string[]> | null>(() => {
|
||||
@@ -187,14 +185,9 @@ export default function LinkSurvey({
|
||||
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
<div />
|
||||
Survey Preview 👀
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded-full bg-slate-500 px-3 py-1 hover:bg-slate-400"
|
||||
onClick={() =>
|
||||
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
||||
}>
|
||||
Restart <RefreshCcwIcon className="ml-2 h-4 w-4" />
|
||||
</button>
|
||||
<ResetProgressButton
|
||||
onClick={() => setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -263,11 +256,13 @@ export default function LinkSurvey({
|
||||
const uploadedUrl = await api.client.storage.uploadFile(file, params);
|
||||
return uploadedUrl;
|
||||
}}
|
||||
onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)}
|
||||
activeQuestionId={activeQuestionId}
|
||||
autoFocus={autoFocus}
|
||||
prefillResponseData={prefillResponseData}
|
||||
responseCount={responseCount}
|
||||
getSetQuestionId={(f: (value: string) => void) => {
|
||||
setQuestionId = f;
|
||||
}}
|
||||
startAtQuestionId={startAt && isStartAtValid ? startAt : undefined}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function FormbricksBranding() {
|
||||
export const FormbricksBranding = () => {
|
||||
return (
|
||||
<a
|
||||
href="https://formbricks.com?utm_source=survey_branding"
|
||||
@@ -13,4 +13,4 @@ export default function FormbricksBranding() {
|
||||
</p>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ interface ProgressBarProps {
|
||||
questionId: string;
|
||||
}
|
||||
|
||||
export default function ProgressBar({ survey, questionId }: ProgressBarProps) {
|
||||
export const ProgressBar = ({ survey, questionId }: ProgressBarProps) => {
|
||||
const currentQuestionIdx = useMemo(
|
||||
() => survey.questions.findIndex((e) => e.id === questionId),
|
||||
[survey, questionId]
|
||||
@@ -43,4 +43,4 @@ export default function ProgressBar({ survey, questionId }: ProgressBarProps) {
|
||||
}, [calculateProgress, survey]);
|
||||
|
||||
return <Progress progress={questionId === "end" ? 1 : progressArray[currentQuestionIdx]} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import AddressQuestion from "@/components/questions/AddressQuestion";
|
||||
import { AddressQuestion } from "@/components/questions/AddressQuestion";
|
||||
import { CTAQuestion } from "@/components/questions/CTAQuestion";
|
||||
import { CalQuestion } from "@/components/questions/CalQuestion";
|
||||
import { ConsentQuestion } from "@/components/questions/ConsentQuestion";
|
||||
@@ -32,7 +32,7 @@ interface QuestionConditionalProps {
|
||||
isInIframe: boolean;
|
||||
}
|
||||
|
||||
export default function QuestionConditional({
|
||||
export const QuestionConditional = ({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -46,7 +46,7 @@ export default function QuestionConditional({
|
||||
surveyId,
|
||||
onFileUpload,
|
||||
isInIframe,
|
||||
}: QuestionConditionalProps) {
|
||||
}: QuestionConditionalProps) => {
|
||||
return question.type === TSurveyQuestionType.OpenText ? (
|
||||
<OpenTextQuestion
|
||||
key={question.id}
|
||||
@@ -242,4 +242,4 @@ export default function QuestionConditional({
|
||||
isInIframe={isInIframe}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import FormbricksBranding from "@/components/general/FormbricksBranding";
|
||||
import ProgressBar from "@/components/general/ProgressBar";
|
||||
import { FormbricksBranding } from "@/components/general/FormbricksBranding";
|
||||
import { ProgressBar } from "@/components/general/ProgressBar";
|
||||
import { QuestionConditional } from "@/components/general/QuestionConditional";
|
||||
import { ResponseErrorComponent } from "@/components/general/ResponseErrorComponent";
|
||||
import { ThankYouCard } from "@/components/general/ThankYouCard";
|
||||
import { WelcomeCard } from "@/components/general/WelcomeCard";
|
||||
import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper";
|
||||
import { evaluateCondition } from "@/lib/logicEvaluator";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -13,17 +16,11 @@ import { SurveyBaseProps } from "@formbricks/types/formbricksSurveys";
|
||||
import type { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import QuestionConditional from "./QuestionConditional";
|
||||
import ThankYouCard from "./ThankYouCard";
|
||||
import WelcomeCard from "./WelcomeCard";
|
||||
|
||||
export function Survey({
|
||||
export const Survey = ({
|
||||
survey,
|
||||
styling,
|
||||
isBrandingEnabled,
|
||||
activeQuestionId,
|
||||
onDisplay = () => {},
|
||||
onActiveQuestionChange = () => {},
|
||||
onResponse = () => {},
|
||||
onClose = () => {},
|
||||
onFinished = () => {},
|
||||
@@ -33,13 +30,15 @@ export function Survey({
|
||||
languageCode,
|
||||
getSetIsError,
|
||||
getSetIsResponseSendingFinished,
|
||||
getSetQuestionId,
|
||||
onFileUpload,
|
||||
responseCount,
|
||||
isCardBorderVisible = true,
|
||||
}: SurveyBaseProps) {
|
||||
startAtQuestionId,
|
||||
}: SurveyBaseProps) => {
|
||||
const isInIframe = window.self !== window.top;
|
||||
const [questionId, setQuestionId] = useState(
|
||||
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
||||
survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id
|
||||
);
|
||||
const [showError, setShowError] = useState(false);
|
||||
// flag state to store whether response processing has been completed or not, we ignore this check for survey editor preview and link survey preview where getSetIsResponseSendingFinished is undefined
|
||||
@@ -65,15 +64,6 @@ export function Survey({
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const showProgressBar = !styling.hideProgressBar;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeQuestionId === "hidden" || activeQuestionId === "multiLanguage") return;
|
||||
if (activeQuestionId === "start" && !survey.welcomeCard.enabled) {
|
||||
setQuestionId(survey?.questions[0]?.id);
|
||||
return;
|
||||
}
|
||||
setQuestionId(activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id));
|
||||
}, [activeQuestionId, survey.questions, survey.welcomeCard.enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
// scroll to top when question changes
|
||||
if (contentRef.current) {
|
||||
@@ -87,6 +77,9 @@ export function Survey({
|
||||
if (prefillResponseData) {
|
||||
onSubmit(prefillResponseData, {}, true);
|
||||
}
|
||||
if (startAtQuestionId && !prefillResponseData) {
|
||||
setQuestionId(startAtQuestionId);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -98,6 +91,14 @@ export function Survey({
|
||||
}
|
||||
}, [getSetIsError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (getSetQuestionId) {
|
||||
getSetQuestionId((value: string) => {
|
||||
setQuestionId(value);
|
||||
});
|
||||
}
|
||||
}, [getSetQuestionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (getSetIsResponseSendingFinished) {
|
||||
getSetIsResponseSendingFinished((value: boolean) => {
|
||||
@@ -174,6 +175,11 @@ export function Survey({
|
||||
}
|
||||
}
|
||||
}
|
||||
// Code to handle case where prefilling and startAt, both are included
|
||||
if (startAtQuestionId && isFormPrefilling && currIdxTemp === 0) {
|
||||
// if isFormPrefilling enabled, then instead of going to the next question in sequence, we go to startAtQuestionId
|
||||
return startAtQuestionId;
|
||||
}
|
||||
return questions[currIdxTemp + 1]?.id || "end";
|
||||
};
|
||||
|
||||
@@ -184,7 +190,7 @@ export function Survey({
|
||||
|
||||
const onSubmit = (responseData: TResponseData, ttc: TResponseTtc, isFormPrefilling: Boolean = false) => {
|
||||
const questionId = Object.keys(responseData)[0];
|
||||
if (isFormPrefilling && questionId === survey.questions[0].id) {
|
||||
if (isFormPrefilling) {
|
||||
onChange(responseData);
|
||||
}
|
||||
setLoadingElement(true);
|
||||
@@ -200,7 +206,6 @@ export function Survey({
|
||||
// add to history
|
||||
setHistory([...history, questionId]);
|
||||
setLoadingElement(false);
|
||||
onActiveQuestionChange(nextQuestionId);
|
||||
};
|
||||
|
||||
const replaceRecallInfo = (text: string): string => {
|
||||
@@ -256,7 +261,6 @@ export function Survey({
|
||||
}
|
||||
if (!prevQuestionId) throw new Error("Question not found");
|
||||
setQuestionId(prevQuestionId);
|
||||
onActiveQuestionChange(prevQuestionId);
|
||||
};
|
||||
|
||||
const getCardContent = (): JSX.Element | undefined => {
|
||||
@@ -348,4 +352,4 @@ export function Survey({
|
||||
</AutoCloseWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,14 +8,12 @@ import { Survey } from "./Survey";
|
||||
export function SurveyModal({
|
||||
survey,
|
||||
isBrandingEnabled,
|
||||
activeQuestionId,
|
||||
getSetIsError,
|
||||
placement,
|
||||
clickOutside,
|
||||
darkOverlay,
|
||||
onDisplay,
|
||||
getSetIsResponseSendingFinished,
|
||||
onActiveQuestionChange,
|
||||
onResponse,
|
||||
onClose,
|
||||
onFinished = () => {},
|
||||
@@ -51,10 +49,8 @@ export function SurveyModal({
|
||||
<Survey
|
||||
survey={survey}
|
||||
isBrandingEnabled={isBrandingEnabled}
|
||||
activeQuestionId={activeQuestionId}
|
||||
onDisplay={onDisplay}
|
||||
getSetIsResponseSendingFinished={getSetIsResponseSendingFinished}
|
||||
onActiveQuestionChange={onActiveQuestionChange}
|
||||
onResponse={onResponse}
|
||||
languageCode={languageCode}
|
||||
onClose={close}
|
||||
|
||||
@@ -22,7 +22,7 @@ interface ThankYouCardProps {
|
||||
isInIframe: boolean;
|
||||
}
|
||||
|
||||
export default function ThankYouCard({
|
||||
export const ThankYouCard = ({
|
||||
headline,
|
||||
subheader,
|
||||
redirectUrl,
|
||||
@@ -35,7 +35,7 @@ export default function ThankYouCard({
|
||||
replaceRecallInfo,
|
||||
isResponseSendingFinished,
|
||||
isInIframe,
|
||||
}: ThankYouCardProps) {
|
||||
}: ThankYouCardProps) => {
|
||||
return (
|
||||
<div className="text-center">
|
||||
{imageUrl || videoUrl ? (
|
||||
@@ -89,4 +89,4 @@ export default function ThankYouCard({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,7 +57,7 @@ const UsersIcon = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default function WelcomeCard({
|
||||
export const WelcomeCard = ({
|
||||
headline,
|
||||
html,
|
||||
fileUrl,
|
||||
@@ -67,7 +67,7 @@ export default function WelcomeCard({
|
||||
survey,
|
||||
responseCount,
|
||||
isInIframe,
|
||||
}: WelcomeCardProps) {
|
||||
}: WelcomeCardProps) => {
|
||||
const calculateTimeToComplete = () => {
|
||||
let idx = calculateElementIdx(survey, 0);
|
||||
if (idx === 0.5) {
|
||||
@@ -149,4 +149,4 @@ export default function WelcomeCard({
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ interface AddressQuestionProps {
|
||||
isInIframe: boolean;
|
||||
}
|
||||
|
||||
export default function AddressQuestion({
|
||||
export const AddressQuestion = ({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -37,7 +37,7 @@ export default function AddressQuestion({
|
||||
ttc,
|
||||
setTtc,
|
||||
isInIframe,
|
||||
}: AddressQuestionProps) {
|
||||
}: AddressQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [hasFilled, setHasFilled] = useState(false);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -179,4 +179,4 @@ export default function AddressQuestion({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,14 +7,13 @@ export interface SurveyBaseProps {
|
||||
survey: TSurvey;
|
||||
styling: TSurveyStyling | TProductStyling;
|
||||
isBrandingEnabled: boolean;
|
||||
activeQuestionId?: string;
|
||||
getSetIsError?: (getSetError: (value: boolean) => void) => void;
|
||||
getSetIsResponseSendingFinished?: (getSetIsResponseSendingFinished: (value: boolean) => void) => void;
|
||||
getSetQuestionId?: (getSetQuestionId: (value: string) => void) => void;
|
||||
onDisplay?: () => void;
|
||||
onResponse?: (response: TResponseUpdate) => void;
|
||||
onFinished?: () => void;
|
||||
onClose?: () => void;
|
||||
onActiveQuestionChange?: (questionId: string) => void;
|
||||
onRetry?: () => void;
|
||||
autoFocus?: boolean;
|
||||
isRedirectDisabled?: boolean;
|
||||
@@ -23,6 +22,7 @@ export interface SurveyBaseProps {
|
||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
||||
responseCount?: number;
|
||||
isCardBorderVisible?: boolean;
|
||||
startAtQuestionId?: string;
|
||||
}
|
||||
|
||||
export interface SurveyInlineProps extends SurveyBaseProps {
|
||||
|
||||
19
packages/ui/ResetProgressButton/index.tsx
Normal file
19
packages/ui/ResetProgressButton/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Repeat2 } from "lucide-react";
|
||||
|
||||
import { Button } from "../Button";
|
||||
|
||||
interface ResetProgressButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ResetProgressButton = ({ onClick }: ResetProgressButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
variant="minimal"
|
||||
className="py-0.2 mr-2 bg-white px-2 font-sans text-sm text-slate-500"
|
||||
onClick={onClick}>
|
||||
Restart
|
||||
<Repeat2 className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user