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:
Piyush Gupta
2024-05-23 17:05:42 +05:30
committed by GitHub
parent 4b13d19ed9
commit 1284adf91d
10 changed files with 130 additions and 93 deletions

View File

@@ -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;

View File

@@ -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];

View File

@@ -1,7 +1,5 @@
import { TI18nString } from "@formbricks/types/surveys";
interface HeadlineProps {
headline?: TI18nString | string;
headline?: string;
questionId: string;
required?: boolean;
alignTextCenter?: boolean;

View File

@@ -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}

View File

@@ -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}
/>
);
};

View File

@@ -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={() => {

View File

@@ -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]);

View 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;
};

View File

@@ -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 {

View File

@@ -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>;