mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-07 08:50:25 -06:00
feat: Add ability to change back button label (#753)
* adds back button label adjust * update wording * made some refactors --------- Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Dhruwang Jariwala <dhruwang@Dhruwangs-MacBook-Pro.local> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
edc8870e09
commit
6a121680ba
@@ -5,6 +5,7 @@ import type { CTAQuestion } from "@formbricks/types/questions";
|
||||
import { Survey } from "@formbricks/types/surveys";
|
||||
import { Editor, Input, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
import { BackButtonInput } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard";
|
||||
|
||||
interface CTAQuestionFormProps {
|
||||
localSurvey: Survey;
|
||||
@@ -80,18 +81,27 @@ export default function CTAQuestionForm({
|
||||
</RadioGroup>
|
||||
|
||||
<div className="mt-3 flex justify-between gap-8">
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
<div className="flex w-full space-x-2">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{questionIdx !== 0 && (
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{question.buttonExternal && (
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="buttonLabel">Button URL</Label>
|
||||
|
||||
@@ -42,6 +42,23 @@ interface QuestionCardProps {
|
||||
isInValid: boolean;
|
||||
}
|
||||
|
||||
export function BackButtonInput({ value, onChange }) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Label htmlFor="backButtonLabel">"Back" Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="backButtonLabel"
|
||||
name="backButtonLabel"
|
||||
value={value}
|
||||
placeholder="Back"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function QuestionCard({
|
||||
localSurvey,
|
||||
questionIdx,
|
||||
@@ -57,6 +74,7 @@ export default function QuestionCard({
|
||||
const question = localSurvey.questions[questionIdx];
|
||||
const open = activeQuestionId === question.id;
|
||||
const [openAdvanced, setOpenAdvanced] = useState(question.logic && question.logic.length > 0);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={question.id} index={questionIdx}>
|
||||
{(provided) => (
|
||||
@@ -210,19 +228,36 @@ export default function QuestionCard({
|
||||
{question.type !== QuestionType.NPS &&
|
||||
question.type !== QuestionType.Rating &&
|
||||
question.type !== QuestionType.CTA ? (
|
||||
<div className="mt-4">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
<div className="mt-4 flex space-x-2">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{questionIdx !== 0 && (
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{(question.type === QuestionType.Rating || question.type === QuestionType.NPS) &&
|
||||
questionIdx !== 0 && (
|
||||
<div className="mt-4">
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AdvancedSettings
|
||||
question={question}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DragDropContext } from "react-beautiful-dnd";
|
||||
import toast from "react-hot-toast";
|
||||
import AddQuestionButton from "./AddQuestionButton";
|
||||
@@ -38,6 +38,8 @@ export default function QuestionsView({
|
||||
}, {});
|
||||
}, []);
|
||||
|
||||
const [backButtonLabel, setbackButtonLabel] = useState(null);
|
||||
|
||||
const handleQuestionLogicChange = (survey: Survey, compareId: string, updatedId: string): Survey => {
|
||||
survey.questions.forEach((question) => {
|
||||
if (!question.logic) return;
|
||||
@@ -68,6 +70,7 @@ export default function QuestionsView({
|
||||
|
||||
const updateQuestion = (questionIdx: number, updatedAttributes: any) => {
|
||||
let updatedSurvey = JSON.parse(JSON.stringify(localSurvey));
|
||||
|
||||
if ("id" in updatedAttributes) {
|
||||
// if the survey whose id is to be changed is linked to logic of any other survey then changing it
|
||||
const initialQuestionId = updatedSurvey.questions[questionIdx].id;
|
||||
@@ -89,6 +92,13 @@ export default function QuestionsView({
|
||||
...updatedSurvey.questions[questionIdx],
|
||||
...updatedAttributes,
|
||||
};
|
||||
|
||||
if ("backButtonLabel" in updatedAttributes) {
|
||||
updatedSurvey.questions.forEach((question) => {
|
||||
question.backButtonLabel = updatedAttributes.backButtonLabel;
|
||||
});
|
||||
setbackButtonLabel(updatedAttributes.backButtonLabel);
|
||||
}
|
||||
setLocalSurvey(updatedSurvey);
|
||||
validateSurvey(updatedSurvey.questions[questionIdx]);
|
||||
};
|
||||
@@ -138,6 +148,9 @@ export default function QuestionsView({
|
||||
|
||||
const addQuestion = (question: any) => {
|
||||
const updatedSurvey = JSON.parse(JSON.stringify(localSurvey));
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
updatedSurvey.questions.push({ ...question, isDraft: true });
|
||||
setLocalSurvey(updatedSurvey);
|
||||
setActiveQuestionId(question.id);
|
||||
|
||||
@@ -2,16 +2,17 @@ import { Button } from "@formbricks/ui";
|
||||
|
||||
interface BackButtonProps {
|
||||
onClick: () => void;
|
||||
backButtonLabel?: string;
|
||||
}
|
||||
|
||||
export function BackButton({ onClick }: BackButtonProps) {
|
||||
export function BackButton({ onClick, backButtonLabel }: BackButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal"
|
||||
className="mr-auto px-3 py-3 text-base font-medium leading-4 focus:ring-offset-2"
|
||||
onClick={() => onClick()}>
|
||||
Back
|
||||
{backButtonLabel || "Back"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export default function CTAQuestion({
|
||||
<HtmlBody htmlString={question.html || ""} questionId={question.id} />
|
||||
|
||||
<div className="mt-4 flex w-full justify-end">
|
||||
{goToPreviousQuestion && <BackButton onClick={() => goToPreviousQuestion()} />}
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton backButtonLabel={question.backButtonLabel} onClick={() => goToPreviousQuestion()} />
|
||||
)}
|
||||
<div></div>
|
||||
{(!question.required || storedResponseValue) && (
|
||||
<button
|
||||
|
||||
@@ -81,6 +81,7 @@ export default function ConsentQuestion({
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() =>
|
||||
goToPreviousQuestion({
|
||||
[question.id]: answer,
|
||||
|
||||
@@ -191,6 +191,7 @@ export default function MultipleChoiceMultiQuestion({
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
if (otherSpecified.length > 0 && showOther) {
|
||||
selectedChoices.push(otherSpecified);
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function MultipleChoiceSingleQuestion({
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion(
|
||||
selectedChoice === "other"
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function NPSQuestion({
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion(
|
||||
storedResponseValue !== selectedChoice
|
||||
|
||||
@@ -83,6 +83,7 @@ export default function OpenTextQuestion({
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion({
|
||||
[question.id]: value,
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function RatingQuestion({
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion({ [question.id]: selectedChoice });
|
||||
}}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { cn } from "@/../../packages/lib/cn";
|
||||
|
||||
interface BackButtonProps {
|
||||
onClick: () => void;
|
||||
backButtonLabel?: string;
|
||||
}
|
||||
|
||||
export function BackButton({ onClick }: BackButtonProps) {
|
||||
export function BackButton({ onClick, backButtonLabel }: BackButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type={"button"}
|
||||
@@ -14,7 +15,7 @@ export function BackButton({ onClick }: BackButtonProps) {
|
||||
"fb-flex fb-items-center fb-rounded-md fb-border fb-border-transparent fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 fb-shadow-sm hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-500 focus:fb-ring-offset-2"
|
||||
)}
|
||||
onClick={onClick}>
|
||||
Back
|
||||
{backButtonLabel ? backButtonLabel : "Back"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,11 @@ export default function CTAQuestion({
|
||||
<HtmlBody htmlString={question.html} questionId={question.id} />
|
||||
|
||||
<div className="fb-mt-4 fb-flex fb-w-full fb-justify-between">
|
||||
<div>{goToPreviousQuestion && <BackButton onClick={() => goToPreviousQuestion()} />}</div>
|
||||
<div>
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton backButtonLabel={question.backButtonLabel} onClick={() => goToPreviousQuestion()} />
|
||||
)}
|
||||
</div>
|
||||
<div className="fb-flex fb-justify-end">
|
||||
{(!question.required || storedResponseValue) && (
|
||||
<button
|
||||
|
||||
@@ -186,6 +186,7 @@ export default function MultipleChoiceMultiQuestion({
|
||||
<div className="fb-mt-4 fb-flex fb-w-full fb-justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
if (otherSpecified.length > 0 && showOther) {
|
||||
selectedChoices.push(otherSpecified);
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function MultipleChoiceSingleQuestion({
|
||||
<div className="fb-mt-4 fb-flex fb-w-full fb-justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion(
|
||||
selectedChoice === "other"
|
||||
|
||||
@@ -97,6 +97,7 @@ export default function NPSQuestion({
|
||||
<div className="fb-mt-4 fb-flex fb-w-full fb-justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion(
|
||||
storedResponseValue !== selectedChoice
|
||||
|
||||
@@ -78,6 +78,7 @@ export default function OpenTextQuestion({
|
||||
<div className="fb-mt-4 fb-flex fb-w-full fb-justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion({
|
||||
[question.id]: value,
|
||||
|
||||
@@ -170,6 +170,7 @@ export default function RatingQuestion({
|
||||
<div className="fb-mt-4 fb-flex fb-w-full fb-justify-between">
|
||||
{goToPreviousQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
goToPreviousQuestion({ [question.id]: selectedChoice });
|
||||
}}
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface IQuestion<T extends Logic> {
|
||||
subheader?: string;
|
||||
required: boolean;
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
logic?: T[];
|
||||
isDraft?: boolean;
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ const ZSurveyQuestionBase = z.object({
|
||||
subheader: z.string().optional(),
|
||||
required: z.boolean(),
|
||||
buttonLabel: z.string().optional(),
|
||||
backButtonLabel: z.string().optional(),
|
||||
scale: z.enum(["number", "smiley", "star"]).optional(),
|
||||
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]).optional(),
|
||||
logic: z.array(ZSurveyLogic).optional(),
|
||||
|
||||
Reference in New Issue
Block a user