mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 10:08:34 -06:00
feat: Extend Prefilling with an option to auto-skip prefilled values (#2598)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { TI18nString } from "@formbricks/types/surveys";
|
||||
|
||||
interface HeadlineProps {
|
||||
headline?: TI18nString | string;
|
||||
headline?: string;
|
||||
questionId: string;
|
||||
required?: boolean;
|
||||
alignTextCenter?: boolean;
|
||||
|
||||
@@ -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 ? (
|
||||
<OpenTextQuestion
|
||||
key={question.id}
|
||||
|
||||
@@ -8,16 +8,13 @@ import { WelcomeCard } from "@/components/general/WelcomeCard";
|
||||
import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper";
|
||||
import { StackedCardsContainer } from "@/components/wrappers/StackedCardsContainer";
|
||||
import { evaluateCondition } from "@/lib/logicEvaluator";
|
||||
import { parseRecallInformation, replaceRecallInfo } from "@/lib/recall";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
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 { SurveyBaseProps } from "@formbricks/types/formbricksSurveys";
|
||||
import type { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
import type { TResponseData, TResponseDataValue, TResponseTtc } from "@formbricks/types/responses";
|
||||
|
||||
export const Survey = ({
|
||||
survey,
|
||||
@@ -30,6 +27,7 @@ export const Survey = ({
|
||||
onRetry = () => {},
|
||||
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 (
|
||||
<ThankYouCard
|
||||
headline={survey.thankYouCard.headline}
|
||||
subheader={survey.thankYouCard.subheader}
|
||||
headline={replaceRecallInfo(
|
||||
getLocalizedValue(survey.thankYouCard.headline, languageCode),
|
||||
responseData
|
||||
)}
|
||||
subheader={replaceRecallInfo(
|
||||
getLocalizedValue(survey.thankYouCard.subheader, languageCode),
|
||||
responseData
|
||||
)}
|
||||
isResponseSendingFinished={isResponseSendingFinished}
|
||||
buttonLabel={survey.thankYouCard.buttonLabel}
|
||||
buttonLabel={getLocalizedValue(survey.thankYouCard.buttonLabel, languageCode)}
|
||||
buttonLink={survey.thankYouCard.buttonLink}
|
||||
imageUrl={survey.thankYouCard.imageUrl}
|
||||
videoUrl={survey.thankYouCard.videoUrl}
|
||||
redirectUrl={survey.redirectUrl}
|
||||
isRedirectDisabled={isRedirectDisabled}
|
||||
languageCode={languageCode}
|
||||
replaceRecallInfo={replaceRecallInfo}
|
||||
isInIframe={isInIframe}
|
||||
/>
|
||||
);
|
||||
@@ -307,7 +282,7 @@ export const Survey = ({
|
||||
question && (
|
||||
<QuestionConditional
|
||||
surveyId={survey.id}
|
||||
question={parseRecallInformation(question)}
|
||||
question={parseRecallInformation(question, languageCode, responseData)}
|
||||
value={responseData[question.id]}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
@@ -315,11 +290,9 @@ export const Survey = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
onFileUpload={onFileUpload}
|
||||
isFirstQuestion={
|
||||
history && prefillResponseData
|
||||
? history[history.length - 1] === survey.questions[0].id
|
||||
: question.id === survey?.questions[0]?.id
|
||||
}
|
||||
isFirstQuestion={question.id === survey?.questions[0]?.id}
|
||||
skipPrefilled={skipPrefilled}
|
||||
prefilledQuestionValue={getQuestionPrefillData(question.id, offset)}
|
||||
isLastQuestion={question.id === survey.questions[survey.questions.length - 1].id}
|
||||
languageCode={languageCode}
|
||||
isInIframe={isInIframe}
|
||||
@@ -359,6 +332,7 @@ export const Survey = ({
|
||||
survey={survey}
|
||||
styling={styling}
|
||||
setQuestionId={setQuestionId}
|
||||
shouldResetQuestionId={shouldResetQuestionId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
<Headline
|
||||
alignTextCenter={true}
|
||||
headline={replaceRecallInfo(getLocalizedValue(headline, languageCode))}
|
||||
questionId="thankYouCard"
|
||||
/>
|
||||
<Subheader
|
||||
subheader={replaceRecallInfo(getLocalizedValue(subheader, languageCode))}
|
||||
questionId="thankYouCard"
|
||||
/>
|
||||
<Headline alignTextCenter={true} headline={headline} questionId="thankYouCard" />
|
||||
<Subheader subheader={subheader} questionId="thankYouCard" />
|
||||
<RedirectCountDown redirectUrl={redirectUrl} isRedirectDisabled={isRedirectDisabled} />
|
||||
{buttonLabel && (
|
||||
<div className="mt-6 flex w-full flex-col items-center justify-center space-y-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||
buttonLabel={buttonLabel}
|
||||
isLastQuestion={false}
|
||||
focus={!isInIframe}
|
||||
onClick={() => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
51
packages/surveys/src/lib/recall.ts
Normal file
51
packages/surveys/src/lib/recall.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -18,12 +18,14 @@ export interface SurveyBaseProps {
|
||||
autoFocus?: boolean;
|
||||
isRedirectDisabled?: boolean;
|
||||
prefillResponseData?: TResponseData;
|
||||
skipPrefilled?: boolean;
|
||||
languageCode: string;
|
||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
||||
responseCount?: number;
|
||||
isCardBorderVisible?: boolean;
|
||||
startAtQuestionId?: string;
|
||||
clickOutside?: boolean;
|
||||
shouldResetQuestionId?: boolean;
|
||||
}
|
||||
|
||||
export interface SurveyInlineProps extends SurveyBaseProps {
|
||||
|
||||
@@ -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<typeof ZResponseDataValue>;
|
||||
|
||||
export const ZResponseData = z.record(ZResponseDataValue);
|
||||
|
||||
export type TResponseData = z.infer<typeof ZResponseData>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user