diff --git a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx index 237afd72b3..cd4dd48c74 100644 --- a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx +++ b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx @@ -49,6 +49,7 @@ export const LinkSurvey = ({ const responseId = singleUseResponse?.id; const searchParams = useSearchParams(); const isPreview = searchParams?.get("preview") === "true"; + const skipPrefilled = searchParams?.get("skipPrefilled") === "true"; const sourceParam = searchParams?.get("source"); const suId = searchParams?.get("suId"); const defaultLanguageCode = survey.languages?.find((surveyLanguage) => { @@ -211,6 +212,7 @@ export const LinkSurvey = ({ styling={determineStyling()} languageCode={languageCode} isBrandingEnabled={product.linkSurveyBranding} + shouldResetQuestionId={false} getSetIsError={(f: (value: boolean) => void) => { setIsError = f; }} @@ -272,6 +274,7 @@ export const LinkSurvey = ({ }} autoFocus={autoFocus} prefillResponseData={prefillValue} + skipPrefilled={skipPrefilled} responseCount={responseCount} getSetQuestionId={(f: (value: string) => void) => { setQuestionId = f; diff --git a/packages/email/components/survey/PreviewEmailTemplate.tsx b/packages/email/components/survey/PreviewEmailTemplate.tsx index 848e86b3dc..6cfefd0c7b 100644 --- a/packages/email/components/survey/PreviewEmailTemplate.tsx +++ b/packages/email/components/survey/PreviewEmailTemplate.tsx @@ -34,7 +34,7 @@ export const getPreviewEmailTemplateHtml = (survey: TSurvey, surveyUrl: string, export const PreviewEmailTemplate = ({ survey, surveyUrl, styling }: PreviewEmailTemplateProps) => { const url = `${surveyUrl}?preview=true`; - const urlWithPrefilling = `${surveyUrl}?preview=true&`; + const urlWithPrefilling = `${surveyUrl}?preview=true&skipPrefilled=true&`; const defaultLanguageCode = "default"; const firstQuestion = survey.questions[0]; diff --git a/packages/surveys/src/components/general/Headline.tsx b/packages/surveys/src/components/general/Headline.tsx index 537c20be0f..e331ce16e9 100644 --- a/packages/surveys/src/components/general/Headline.tsx +++ b/packages/surveys/src/components/general/Headline.tsx @@ -1,7 +1,5 @@ -import { TI18nString } from "@formbricks/types/surveys"; - interface HeadlineProps { - headline?: TI18nString | string; + headline?: string; questionId: string; required?: boolean; alignTextCenter?: boolean; diff --git a/packages/surveys/src/components/general/QuestionConditional.tsx b/packages/surveys/src/components/general/QuestionConditional.tsx index 74d5ebf8bd..2e76a1ba64 100644 --- a/packages/surveys/src/components/general/QuestionConditional.tsx +++ b/packages/surveys/src/components/general/QuestionConditional.tsx @@ -12,7 +12,7 @@ import { OpenTextQuestion } from "@/components/questions/OpenTextQuestion"; import { PictureSelectionQuestion } from "@/components/questions/PictureSelectionQuestion"; import { RatingQuestion } from "@/components/questions/RatingQuestion"; -import { TResponseData, TResponseTtc } from "@formbricks/types/responses"; +import { TResponseData, TResponseDataValue, TResponseTtc } from "@formbricks/types/responses"; import { TUploadFileConfig } from "@formbricks/types/storage"; import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys"; @@ -26,6 +26,8 @@ interface QuestionConditionalProps { isFirstQuestion: boolean; isLastQuestion: boolean; languageCode: string; + prefilledQuestionValue?: TResponseDataValue; + skipPrefilled?: boolean; ttc: TResponseTtc; setTtc: (ttc: TResponseTtc) => void; surveyId: string; @@ -42,6 +44,8 @@ export const QuestionConditional = ({ isFirstQuestion, isLastQuestion, languageCode, + prefilledQuestionValue, + skipPrefilled, ttc, setTtc, surveyId, @@ -49,6 +53,14 @@ export const QuestionConditional = ({ isInIframe, currentQuestionId, }: QuestionConditionalProps) => { + if (!value && prefilledQuestionValue) { + if (skipPrefilled) { + onSubmit({ [question.id]: prefilledQuestionValue }, { [question.id]: 0 }); + } else { + onChange({ [question.id]: prefilledQuestionValue }); + } + } + return question.type === TSurveyQuestionType.OpenText ? ( {}, isRedirectDisabled = false, prefillResponseData, + skipPrefilled, languageCode, getSetIsError, getSetIsResponseSendingFinished, @@ -38,11 +36,20 @@ export const Survey = ({ responseCount, startAtQuestionId, clickOutside, + shouldResetQuestionId, }: SurveyBaseProps) => { const isInIframe = window.self !== window.top; - const [questionId, setQuestionId] = useState( - survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id - ); + + const [questionId, setQuestionId] = useState(() => { + if (startAtQuestionId) { + return startAtQuestionId; + } else if (survey.welcomeCard.enabled) { + return "start"; + } else { + return 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 const [isResponseSendingFinished, setIsResponseSendingFinished] = useState( @@ -87,12 +94,7 @@ export const Survey = ({ useEffect(() => { // call onDisplay when component is mounted onDisplay(); - if (prefillResponseData) { - onChange(prefillResponseData); - } - if (startAtQuestionId) { - setQuestionId(startAtQuestionId); - } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -196,6 +198,7 @@ export const Survey = ({ setLoadingElement(true); const nextQuestionId = getNextQuestionId(responseData); const finished = nextQuestionId === "end"; + onChange(responseData); onResponse({ data: responseData, ttc, finished }); if (finished) { // Post a message to the parent window indicating that the survey is completed. @@ -208,45 +211,6 @@ export const Survey = ({ setLoadingElement(false); }; - const replaceRecallInfo = (text: string): string => { - while (text.includes("recall:")) { - const recallInfo = extractRecallInfo(text); - if (recallInfo) { - const questionId = extractId(recallInfo); - const fallback = extractFallbackValue(recallInfo).replaceAll("nbsp", " "); - let value = questionId && responseData[questionId] ? (responseData[questionId] as string) : fallback; - - if (isValidDateString(value)) { - value = formatDateWithOrdinal(new Date(value)); - } - if (Array.isArray(value)) { - value = value.filter((item) => item !== null && item !== undefined && item !== "").join(", "); - } - text = text.replace(recallInfo, value); - } - } - return text; - }; - - const parseRecallInformation = (question: TSurveyQuestion) => { - const modifiedQuestion = structuredClone(question); - if (question.headline && question.headline[languageCode]?.includes("recall:")) { - modifiedQuestion.headline[languageCode] = replaceRecallInfo( - getLocalizedValue(modifiedQuestion.headline, languageCode) - ); - } - if ( - question.subheader && - question.subheader[languageCode]?.includes("recall:") && - modifiedQuestion.subheader - ) { - modifiedQuestion.subheader[languageCode] = replaceRecallInfo( - getLocalizedValue(modifiedQuestion.subheader, languageCode) - ); - } - return modifiedQuestion; - }; - const onBack = (): void => { let prevQuestionId; // use history if available @@ -262,6 +226,13 @@ export const Survey = ({ setQuestionId(prevQuestionId); }; + const getQuestionPrefillData = (questionId: string, offset: number): TResponseDataValue | undefined => { + if (offset === 0 && prefillResponseData) { + return prefillResponseData[questionId]; + } + return undefined; + }; + const getCardContent = (questionIdx: number, offset: number): JSX.Element | undefined => { if (showError) { return ( @@ -287,17 +258,21 @@ export const Survey = ({ } else if (questionIdx === survey.questions.length) { return ( ); @@ -307,7 +282,7 @@ export const Survey = ({ question && ( ); }; diff --git a/packages/surveys/src/components/general/ThankYouCard.tsx b/packages/surveys/src/components/general/ThankYouCard.tsx index 1dbb289b7c..7dcfbe94f8 100644 --- a/packages/surveys/src/components/general/ThankYouCard.tsx +++ b/packages/surveys/src/components/general/ThankYouCard.tsx @@ -6,20 +6,15 @@ import { RedirectCountDown } from "@/components/general/RedirectCountdown"; import { Subheader } from "@/components/general/Subheader"; import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { TI18nString } from "@formbricks/types/surveys"; - interface ThankYouCardProps { - headline?: TI18nString; - subheader?: TI18nString; + headline?: string; + subheader?: string; redirectUrl: string | null; isRedirectDisabled: boolean; - languageCode: string; - buttonLabel?: TI18nString; + buttonLabel?: string; buttonLink?: string; imageUrl?: string; videoUrl?: string; - replaceRecallInfo: (text: string) => string; isResponseSendingFinished: boolean; isInIframe: boolean; } @@ -29,12 +24,10 @@ export const ThankYouCard = ({ subheader, redirectUrl, isRedirectDisabled, - languageCode, buttonLabel, buttonLink, imageUrl, videoUrl, - replaceRecallInfo, isResponseSendingFinished, isInIframe, }: ThankYouCardProps) => { @@ -64,20 +57,13 @@ export const ThankYouCard = ({ {isResponseSendingFinished ? ( <> {media || checkmark} - - + + {buttonLabel && (
{ diff --git a/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx b/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx index b45f93cffa..b137a80d38 100644 --- a/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx +++ b/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx @@ -13,6 +13,7 @@ interface StackedCardsContainerProps { getCardContent: (questionIdx: number, offset: number) => JSX.Element | undefined; styling: TProductStyling | TSurveyStyling; setQuestionId: (questionId: string) => void; + shouldResetQuestionId?: boolean; } export const StackedCardsContainer = ({ @@ -22,6 +23,7 @@ export const StackedCardsContainer = ({ getCardContent, styling, setQuestionId, + shouldResetQuestionId = true, }: StackedCardsContainerProps) => { const [hovered, setHovered] = useState(false); const highlightBorderColor = @@ -100,7 +102,9 @@ export const StackedCardsContainer = ({ // Reset question progress, when card arrangement changes useEffect(() => { - setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id); + if (shouldResetQuestionId) { + setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [cardArrangement]); diff --git a/packages/surveys/src/lib/recall.ts b/packages/surveys/src/lib/recall.ts new file mode 100644 index 0000000000..53f7fc2126 --- /dev/null +++ b/packages/surveys/src/lib/recall.ts @@ -0,0 +1,51 @@ +import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; +import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone"; +import { formatDateWithOrdinal, isValidDateString } from "@formbricks/lib/utils/datetime"; +import { extractFallbackValue, extractId, extractRecallInfo } from "@formbricks/lib/utils/recall"; +import { TResponseData } from "@formbricks/types/responses"; +import { TSurveyQuestion } from "@formbricks/types/surveys"; + +export const replaceRecallInfo = (text: string, responseData: TResponseData): string => { + while (text.includes("recall:")) { + const recallInfo = extractRecallInfo(text); + if (recallInfo) { + const questionId = extractId(recallInfo); + const fallback = extractFallbackValue(recallInfo).replaceAll("nbsp", " "); + let value = questionId && responseData[questionId] ? (responseData[questionId] as string) : fallback; + + if (isValidDateString(value)) { + value = formatDateWithOrdinal(new Date(value)); + } + if (Array.isArray(value)) { + value = value.filter((item) => item !== null && item !== undefined && item !== "").join(", "); + } + text = text.replace(recallInfo, value); + } + } + return text; +}; + +export const parseRecallInformation = ( + question: TSurveyQuestion, + languageCode: string, + responseData: TResponseData +) => { + const modifiedQuestion = structuredClone(question); + if (question.headline && question.headline[languageCode]?.includes("recall:")) { + modifiedQuestion.headline[languageCode] = replaceRecallInfo( + getLocalizedValue(modifiedQuestion.headline, languageCode), + responseData + ); + } + if ( + question.subheader && + question.subheader[languageCode]?.includes("recall:") && + modifiedQuestion.subheader + ) { + modifiedQuestion.subheader[languageCode] = replaceRecallInfo( + getLocalizedValue(modifiedQuestion.subheader, languageCode), + responseData + ); + } + return modifiedQuestion; +}; diff --git a/packages/types/formbricksSurveys.ts b/packages/types/formbricksSurveys.ts index 416838c8cb..935e9b0c66 100644 --- a/packages/types/formbricksSurveys.ts +++ b/packages/types/formbricksSurveys.ts @@ -18,12 +18,14 @@ export interface SurveyBaseProps { autoFocus?: boolean; isRedirectDisabled?: boolean; prefillResponseData?: TResponseData; + skipPrefilled?: boolean; languageCode: string; onFileUpload: (file: File, config?: TUploadFileConfig) => Promise; responseCount?: number; isCardBorderVisible?: boolean; startAtQuestionId?: string; clickOutside?: boolean; + shouldResetQuestionId?: boolean; } export interface SurveyInlineProps extends SurveyBaseProps { diff --git a/packages/types/responses.ts b/packages/types/responses.ts index 19bb2e592c..07a81eb33f 100644 --- a/packages/types/responses.ts +++ b/packages/types/responses.ts @@ -5,9 +5,16 @@ import { ZId } from "./environment"; import { ZSurvey, ZSurveyLogicCondition } from "./surveys"; import { ZTag } from "./tags"; -export const ZResponseData = z.record( - z.union([z.string(), z.number(), z.array(z.string()), z.record(z.string())]) -); +export const ZResponseDataValue = z.union([ + z.string(), + z.number(), + z.array(z.string()), + z.record(z.string()), +]); + +export type TResponseDataValue = z.infer; + +export const ZResponseData = z.record(ZResponseDataValue); export type TResponseData = z.infer;