diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts index 895814f133..275d965747 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts @@ -61,6 +61,7 @@ export const getSurveysForEnvironmentState = reactCache( displayLimit: true, displayOption: true, hiddenFields: true, + isBackButtonHidden: true, triggers: { select: { actionClass: { diff --git a/apps/web/app/lib/templates.ts b/apps/web/app/lib/templates.ts index d4ae069ea2..f8042c6ad5 100644 --- a/apps/web/app/lib/templates.ts +++ b/apps/web/app/lib/templates.ts @@ -7064,5 +7064,6 @@ export const previewSurvey = (projectName: string, t: TFnType) => { triggers: [], showLanguageSwitch: false, followUps: [], + isBackButtonHidden: false, } as TSurvey; }; diff --git a/apps/web/modules/survey/editor/components/response-options-card.tsx b/apps/web/modules/survey/editor/components/response-options-card.tsx index 57fb9e4bce..08b29056c8 100644 --- a/apps/web/modules/survey/editor/components/response-options-card.tsx +++ b/apps/web/modules/survey/editor/components/response-options-card.tsx @@ -205,6 +205,10 @@ export const ResponseOptionsCard = ({ } }; + const handleHideBackButtonToggle = () => { + setLocalSurvey({ ...localSurvey, isBackButtonHidden: !localSurvey.isBackButtonHidden }); + }; + useEffect(() => { if (!!localSurvey.surveyClosedMessage) { setSurveyClosedMessage({ @@ -515,6 +519,13 @@ export const ResponseOptionsCard = ({ )} + diff --git a/apps/web/modules/survey/lib/survey.ts b/apps/web/modules/survey/lib/survey.ts index ba35bc36bf..af624428db 100644 --- a/apps/web/modules/survey/lib/survey.ts +++ b/apps/web/modules/survey/lib/survey.ts @@ -41,6 +41,7 @@ export const selectSurvey = { pin: true, resultShareKey: true, showLanguageSwitch: true, + isBackButtonHidden: true, languages: { select: { default: true, diff --git a/apps/web/modules/survey/templates/lib/minimal-survey.ts b/apps/web/modules/survey/templates/lib/minimal-survey.ts index 91d9cd0ebf..968a517b6e 100644 --- a/apps/web/modules/survey/templates/lib/minimal-survey.ts +++ b/apps/web/modules/survey/templates/lib/minimal-survey.ts @@ -41,4 +41,5 @@ export const getMinimalSurvey = (t: TFnType): TSurvey => ({ isSingleResponsePerEmailEnabled: false, variables: [], followUps: [], + isBackButtonHidden: false, }); diff --git a/docs/images/xm-and-surveys/surveys/general-features/hide-back-button/hide-back-button.webp b/docs/images/xm-and-surveys/surveys/general-features/hide-back-button/hide-back-button.webp new file mode 100644 index 0000000000..88c1a8bca3 Binary files /dev/null and b/docs/images/xm-and-surveys/surveys/general-features/hide-back-button/hide-back-button.webp differ diff --git a/docs/mint.json b/docs/mint.json index 057ec28620..9187d59c7d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -47,7 +47,8 @@ "xm-and-surveys/surveys/general-features/shareable-dashboards", "xm-and-surveys/surveys/general-features/schedule-start-end-dates", "xm-and-surveys/surveys/general-features/metadata", - "xm-and-surveys/surveys/general-features/variables" + "xm-and-surveys/surveys/general-features/variables", + "xm-and-surveys/surveys/general-features/hide-back-button" ] }, { diff --git a/docs/xm-and-surveys/surveys/general-features/hide-back-button.mdx b/docs/xm-and-surveys/surveys/general-features/hide-back-button.mdx new file mode 100644 index 0000000000..dcdb492f8c --- /dev/null +++ b/docs/xm-and-surveys/surveys/general-features/hide-back-button.mdx @@ -0,0 +1,11 @@ +--- +title: Hide Back Button +description: Learn how to hide the back button in surveys. +icon: arrow-left +--- + +Surveys display a back button by default. If you want to prevent respondents from returning to previous questions, you'll need to disable this feature explicitly. + +To disable the back button, navigate to the survey settings and select the Response options tab. + +![Hide back button](/images/xm-and-surveys/surveys/general-features/hide-back-button/hide-back-button.webp) diff --git a/packages/database/migration/20250226080646_set_response_updated_at_default/migration.sql b/packages/database/migration/20250226080646_set_response_updated_at_default/migration.sql new file mode 100644 index 0000000000..e2d04b4ec2 --- /dev/null +++ b/packages/database/migration/20250226080646_set_response_updated_at_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Response" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/packages/database/migration/20250226080718_add_is_back_button_hidden_to_survey/migration.sql b/packages/database/migration/20250226080718_add_is_back_button_hidden_to_survey/migration.sql new file mode 100644 index 0000000000..bdf64cfc4d --- /dev/null +++ b/packages/database/migration/20250226080718_add_is_back_button_hidden_to_survey/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Survey" ADD COLUMN "isBackButtonHidden" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 904274d903..d9781e2809 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -410,6 +410,7 @@ model Survey { verifyEmail Json? // deprecated isVerifyEmailEnabled Boolean @default(false) isSingleResponsePerEmailEnabled Boolean @default(false) + isBackButtonHidden Boolean @default(false) pin String? resultShareKey String? @unique displayPercentage Decimal? diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index 6aa137c92f..d9da25a217 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -69,6 +69,7 @@ export const selectSurvey = { autoComplete: true, isVerifyEmailEnabled: true, isSingleResponsePerEmailEnabled: true, + isBackButtonHidden: true, redirectUrl: true, projectOverwrites: true, styling: true, diff --git a/packages/lib/survey/tests/__mock__/survey.mock.ts b/packages/lib/survey/tests/__mock__/survey.mock.ts index 0031ebdced..cca45f9d37 100644 --- a/packages/lib/survey/tests/__mock__/survey.mock.ts +++ b/packages/lib/survey/tests/__mock__/survey.mock.ts @@ -185,6 +185,7 @@ const baseSurveyProperties = { displayLimit: 3, welcomeCard: mockWelcomeCard, questions: [mockQuestion], + isBackButtonHidden: false, endings: [ { id: "umyknohldc7w26ocjdhaa62c", diff --git a/packages/surveys/src/components/general/question-conditional.tsx b/packages/surveys/src/components/general/question-conditional.tsx index 4c1fec2821..b67be6f3c7 100644 --- a/packages/surveys/src/components/general/question-conditional.tsx +++ b/packages/surveys/src/components/general/question-conditional.tsx @@ -41,6 +41,7 @@ interface QuestionConditionalProps { surveyId: string; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function QuestionConditional({ @@ -60,6 +61,7 @@ export function QuestionConditional({ onFileUpload, autoFocusEnabled, currentQuestionId, + isBackButtonHidden, }: QuestionConditionalProps) { const getResponseValueForRankingQuestion = ( value: string[], @@ -93,6 +95,7 @@ export function QuestionConditional({ setTtc={setTtc} autoFocusEnabled={autoFocusEnabled} currentQuestionId={currentQuestionId} + isBackButtonHidden={isBackButtonHidden} /> ) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? ( ) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ? ( ) : question.type === TSurveyQuestionTypeEnum.NPS ? ( ) : question.type === TSurveyQuestionTypeEnum.CTA ? ( ) : question.type === TSurveyQuestionTypeEnum.Rating ? ( ) : question.type === TSurveyQuestionTypeEnum.Consent ? ( ) : question.type === TSurveyQuestionTypeEnum.Date ? ( ) : question.type === TSurveyQuestionTypeEnum.PictureSelection ? ( ) : question.type === TSurveyQuestionTypeEnum.FileUpload ? ( ) : question.type === TSurveyQuestionTypeEnum.Cal ? ( ) : question.type === TSurveyQuestionTypeEnum.Matrix ? ( ) : question.type === TSurveyQuestionTypeEnum.Address ? ( ) : question.type === TSurveyQuestionTypeEnum.Ranking ? ( ) : question.type === TSurveyQuestionTypeEnum.ContactInfo ? ( ) : null; } diff --git a/packages/surveys/src/components/general/survey.tsx b/packages/surveys/src/components/general/survey.tsx index 1b4982d9bf..2ecda6ba2c 100644 --- a/packages/surveys/src/components/general/survey.tsx +++ b/packages/surveys/src/components/general/survey.tsx @@ -399,6 +399,7 @@ export function Survey({ languageCode={selectedLanguage} autoFocusEnabled={autoFocusEnabled} currentQuestionId={questionId} + isBackButtonHidden={localSurvey.isBackButtonHidden} /> ) ); diff --git a/packages/surveys/src/components/questions/address-question.tsx b/packages/surveys/src/components/questions/address-question.tsx index bd8c51aa0a..388e3f39f3 100644 --- a/packages/surveys/src/components/questions/address-question.tsx +++ b/packages/surveys/src/components/questions/address-question.tsx @@ -25,6 +25,7 @@ interface AddressQuestionProps { setTtc: (ttc: TResponseTtc) => void; currentQuestionId: TSurveyQuestionId; autoFocusEnabled: boolean; + isBackButtonHidden: boolean; } export function AddressQuestion({ @@ -40,6 +41,7 @@ export function AddressQuestion({ setTtc, currentQuestionId, autoFocusEnabled, + isBackButtonHidden, }: AddressQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -179,7 +181,7 @@ export function AddressQuestion({ isLastQuestion={isLastQuestion} />
- {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function CalQuestion({ @@ -38,6 +39,7 @@ export function CalQuestion({ ttc, setTtc, currentQuestionId, + isBackButtonHidden, }: CalQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -95,7 +97,7 @@ export function CalQuestion({ /> )}
- {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( { diff --git a/packages/surveys/src/components/questions/consent-question.tsx b/packages/surveys/src/components/questions/consent-question.tsx index 4576978ae0..e5b1a5a822 100644 --- a/packages/surveys/src/components/questions/consent-question.tsx +++ b/packages/surveys/src/components/questions/consent-question.tsx @@ -23,6 +23,7 @@ interface ConsentQuestionProps { setTtc: (ttc: TResponseTtc) => void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function ConsentQuestion({ @@ -38,6 +39,7 @@ export function ConsentQuestion({ setTtc, currentQuestionId, autoFocusEnabled, + isBackButtonHidden, }: ConsentQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -126,7 +128,7 @@ export function ConsentQuestion({ isLastQuestion={isLastQuestion} />
- {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; currentQuestionId: TSurveyQuestionId; autoFocusEnabled: boolean; + isBackButtonHidden: boolean; } export function ContactInfoQuestion({ @@ -40,6 +41,7 @@ export function ContactInfoQuestion({ setTtc, currentQuestionId, autoFocusEnabled, + isBackButtonHidden, }: ContactInfoQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -181,7 +183,7 @@ export function ContactInfoQuestion({ buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)} isLastQuestion={isLastQuestion} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function CTAQuestion({ @@ -37,6 +38,7 @@ export function CTAQuestion({ setTtc, autoFocusEnabled, currentQuestionId, + isBackButtonHidden, }: CTAQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -92,7 +94,7 @@ export function CTAQuestion({ )}
- {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } function CalendarIcon() { @@ -91,6 +92,7 @@ export function DateQuestion({ setTtc, ttc, currentQuestionId, + isBackButtonHidden, }: DateQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const [errorMessage, setErrorMessage] = useState(""); @@ -272,7 +274,7 @@ export function DateQuestion({ isLastQuestion={isLastQuestion} buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function FileUploadQuestion({ @@ -44,6 +45,7 @@ export function FileUploadQuestion({ ttc, setTtc, currentQuestionId, + isBackButtonHidden, }: FileUploadQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -110,7 +112,7 @@ export function FileUploadQuestion({ buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)} isLastQuestion={isLastQuestion} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function MatrixQuestion({ @@ -38,6 +39,7 @@ export function MatrixQuestion({ ttc, setTtc, currentQuestionId, + isBackButtonHidden, }: MatrixQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -210,7 +212,7 @@ export function MatrixQuestion({ isLastQuestion={isLastQuestion} tabIndex={isCurrent ? 0 : -1} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function MultipleChoiceMultiQuestion({ @@ -39,6 +40,7 @@ export function MultipleChoiceMultiQuestion({ setTtc, autoFocusEnabled, currentQuestionId, + isBackButtonHidden, }: MultipleChoiceMultiProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -293,7 +295,7 @@ export function MultipleChoiceMultiQuestion({ buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)} isLastQuestion={isLastQuestion} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function MultipleChoiceSingleQuestion({ @@ -39,6 +40,7 @@ export function MultipleChoiceSingleQuestion({ setTtc, autoFocusEnabled, currentQuestionId, + isBackButtonHidden, }: MultipleChoiceSingleProps) { const [startTime, setStartTime] = useState(performance.now()); const [otherSelected, setOtherSelected] = useState(false); @@ -250,7 +252,7 @@ export function MultipleChoiceSingleQuestion({ buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)} isLastQuestion={isLastQuestion} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function NPSQuestion({ @@ -38,6 +39,7 @@ export function NPSQuestion({ ttc, setTtc, currentQuestionId, + isBackButtonHidden, }: NPSQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const [hoveredNumber, setHoveredNumber] = useState(-1); @@ -153,14 +155,16 @@ export function NPSQuestion({
- {!question.required && ( + {question.required ? ( + <> + ) : ( )} - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function OpenTextQuestion({ @@ -40,6 +41,7 @@ export function OpenTextQuestion({ setTtc, autoFocusEnabled, currentQuestionId, + isBackButtonHidden, }: OpenTextQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const [currentLength, setCurrentLength] = useState(value.length || 0); @@ -161,7 +163,7 @@ export function OpenTextQuestion({ isLastQuestion={isLastQuestion} onClick={() => {}} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function PictureSelectionQuestion({ @@ -39,6 +40,7 @@ export function PictureSelectionQuestion({ ttc, setTtc, currentQuestionId, + isBackButtonHidden, }: PictureSelectionProps) { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; @@ -209,7 +211,7 @@ export function PictureSelectionQuestion({ buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)} isLastQuestion={isLastQuestion} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function RankingQuestion({ @@ -44,6 +45,7 @@ export function RankingQuestion({ setTtc, autoFocusEnabled, currentQuestionId, + isBackButtonHidden, }: RankingQuestionProps) { const [startTime, setStartTime] = useState(performance.now()); const isCurrent = question.id === currentQuestionId; @@ -272,7 +274,7 @@ export function RankingQuestion({ buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)} isLastQuestion={isLastQuestion} /> - {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( void; autoFocusEnabled: boolean; currentQuestionId: TSurveyQuestionId; + isBackButtonHidden: boolean; } export function RatingQuestion({ @@ -51,6 +52,7 @@ export function RatingQuestion({ ttc, setTtc, currentQuestionId, + isBackButtonHidden, }: RatingQuestionProps) { const [hoveredNumber, setHoveredNumber] = useState(0); const [startTime, setStartTime] = useState(performance.now()); @@ -259,7 +261,9 @@ export function RatingQuestion({
- {!question.required && ( + {question.required ? ( + <> + ) : ( )}
- {!isFirstQuestion && ( + {!isFirstQuestion && !isBackButtonHidden && ( null); diff --git a/packages/types/surveys/types.ts b/packages/types/surveys/types.ts index e4bbc3c553..a541555266 100644 --- a/packages/types/surveys/types.ts +++ b/packages/types/surveys/types.ts @@ -868,13 +868,14 @@ export const ZSurvey = z singleUse: ZSurveySingleUse.nullable(), isVerifyEmailEnabled: z.boolean(), isSingleResponsePerEmailEnabled: z.boolean(), + isBackButtonHidden: z.boolean(), pin: z.string().min(4, { message: "PIN must be a four digit number" }).nullish(), resultShareKey: z.string().nullable(), displayPercentage: z.number().min(0.01).max(100).nullable(), languages: z.array(ZSurveyLanguage), }) .superRefine((survey, ctx) => { - const { questions, languages, welcomeCard, endings } = survey; + const { questions, languages, welcomeCard, endings, isBackButtonHidden } = survey; let multiLangIssue: z.IssueData | null; @@ -943,7 +944,9 @@ export const ZSurvey = z ]; const fieldsToValidate = - questionIndex === 0 ? initialFieldsToValidate : [...initialFieldsToValidate, "backButtonLabel"]; + questionIndex === 0 || isBackButtonHidden + ? initialFieldsToValidate + : [...initialFieldsToValidate, "backButtonLabel"]; for (const field of fieldsToValidate) { // Skip label validation for consent questions as its called checkbox label