diff --git a/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--dark.png b/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--dark.png index f427261f0c801..a820c399ecbf2 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--dark.png and b/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--light.png b/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--light.png index fa79b1b313e1d..1276a2564f958 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--light.png and b/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight--light.png differ diff --git a/frontend/src/scenes/surveys/QuestionBranchingInput.tsx b/frontend/src/scenes/surveys/QuestionBranchingInput.tsx index 96c6ea55912d6..be078d80f050f 100644 --- a/frontend/src/scenes/surveys/QuestionBranchingInput.tsx +++ b/frontend/src/scenes/surveys/QuestionBranchingInput.tsx @@ -3,8 +3,9 @@ import './EditSurvey.scss' import { LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { LemonField } from 'lib/lemon-ui/LemonField' +import { truncate } from 'lib/utils' -import { MultipleSurveyQuestion, RatingSurveyQuestion, SurveyQuestionBranchingType } from '~/types' +import { MultipleSurveyQuestion, RatingSurveyQuestion, SurveyQuestionBranchingType, SurveyQuestionType } from '~/types' import { surveyLogic } from './surveyLogic' @@ -16,7 +17,7 @@ export function QuestionBranchingInput({ question: RatingSurveyQuestion | MultipleSurveyQuestion }): JSX.Element { const { survey, getBranchingDropdownValue } = useValues(surveyLogic) - const { setQuestionBranching } = useActions(surveyLogic) + const { setQuestionBranchingType } = useActions(surveyLogic) const availableNextQuestions = survey.questions .map((question, questionIndex) => ({ @@ -25,6 +26,8 @@ export function QuestionBranchingInput({ })) .filter((_, idx) => questionIndex !== idx) const branchingDropdownValue = getBranchingDropdownValue(questionIndex, question) + const hasResponseBasedBranching = + question.type === SurveyQuestionType.Rating || question.type === SurveyQuestionType.SingleChoice return ( <> @@ -33,7 +36,14 @@ export function QuestionBranchingInput({ className="max-w-80 whitespace-nowrap" value={branchingDropdownValue} data-attr={`branching-question-${questionIndex}`} - onSelect={(value) => setQuestionBranching(questionIndex, value)} + onSelect={(type) => { + let specificQuestionIndex + if (type.startsWith(SurveyQuestionBranchingType.SpecificQuestion)) { + specificQuestionIndex = parseInt(type.split(':')[1]) + type = SurveyQuestionBranchingType.SpecificQuestion + } + setQuestionBranchingType(questionIndex, type, specificQuestionIndex) + }} options={[ ...(questionIndex < survey.questions.length - 1 ? [ @@ -47,22 +57,122 @@ export function QuestionBranchingInput({ label: 'Confirmation message', value: SurveyQuestionBranchingType.ConfirmationMessage, }, - { - label: 'Specific question based on answer', - value: SurveyQuestionBranchingType.ResponseBased, - }, + ...(hasResponseBasedBranching + ? [ + { + label: 'Specific question based on answer', + value: SurveyQuestionBranchingType.ResponseBased, + }, + ] + : []), ...availableNextQuestions.map((question) => ({ - label: `${question.questionIndex + 1}. ${question.question}`, + label: truncate(`${question.questionIndex + 1}. ${question.question}`, 40), value: `${SurveyQuestionBranchingType.SpecificQuestion}:${question.questionIndex}`, })), ]} /> {branchingDropdownValue === SurveyQuestionBranchingType.ResponseBased && ( -
- TODO: dropdowns for the response-based branching -
+ )} ) } + +function QuestionResponseBasedBranchingInput({ + questionIndex, + question, +}: { + questionIndex: number + question: RatingSurveyQuestion | MultipleSurveyQuestion +}): JSX.Element { + const { survey, getResponseBasedBranchingDropdownValue } = useValues(surveyLogic) + const { setResponseBasedBranchingForQuestion } = useActions(surveyLogic) + + const availableNextQuestions = survey.questions + .map((question, questionIndex) => ({ + ...question, + questionIndex, + })) + .filter((_, idx) => questionIndex !== idx) + + let config: { value: string | number; label: string }[] = [] + + if (question.type === SurveyQuestionType.Rating && question.scale === 3) { + config = [ + { value: 'negative', label: '1 (Negative)' }, + { value: 'neutral', label: '2 (Neutral)' }, + { value: 'positive', label: '3 (Positive)' }, + ] + } else if (question.type === SurveyQuestionType.Rating && question.scale === 5) { + config = [ + { value: 'negative', label: '1 to 2 (Negative)' }, + { value: 'neutral', label: '3 (Neutral)' }, + { value: 'positive', label: '4 to 5 (Positive)' }, + ] + } else if (question.type === SurveyQuestionType.Rating && question.scale === 10) { + config = [ + // NPS categories + { value: 'detractors', label: '0 to 6 (Detractors)' }, + { value: 'passives', label: '7 to 8 (Passives)' }, + { value: 'promoters', label: '9 to 10 (Promoters)' }, + ] + } else if (question.type === SurveyQuestionType.SingleChoice) { + config = question.choices.map((choice, choiceIndex) => ({ + value: choiceIndex, + label: `Option ${choiceIndex + 1} ("${truncate(choice, 15)}")`, + })) + } + + return ( +
+ {config.map(({ value, label }, i) => ( +
+
+
+ If the answer is {label}, go to: +
+
+
+ { + let specificQuestionIndex + if (nextStep.startsWith(SurveyQuestionBranchingType.SpecificQuestion)) { + specificQuestionIndex = parseInt(nextStep.split(':')[1]) + nextStep = SurveyQuestionBranchingType.SpecificQuestion + } + setResponseBasedBranchingForQuestion( + questionIndex, + value, + nextStep, + specificQuestionIndex + ) + }} + options={[ + ...(questionIndex < survey.questions.length - 1 + ? [ + { + label: 'Next question', + value: SurveyQuestionBranchingType.NextQuestion, + }, + ] + : []), + { + label: 'Confirmation message', + value: SurveyQuestionBranchingType.ConfirmationMessage, + }, + ...availableNextQuestions.map((question) => ({ + label: truncate(`${question.questionIndex + 1}. ${question.question}`, 20), + value: `${SurveyQuestionBranchingType.SpecificQuestion}:${question.questionIndex}`, + })), + ]} + /> +
+
+ ))} +
+ ) +} diff --git a/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx index ec3b03d54b41d..e1b24b1bea182 100644 --- a/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx +++ b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx @@ -87,11 +87,9 @@ export function SurveyEditQuestionHeader({ export function SurveyEditQuestionGroup({ index, question }: { index: number; question: any }): JSX.Element { const { survey, descriptionContentType } = useValues(surveyLogic) - const { setDefaultForQuestionType, setSurveyValue } = useActions(surveyLogic) + const { setDefaultForQuestionType, setSurveyValue, resetBranchingForQuestion } = useActions(surveyLogic) const { featureFlags } = useValues(enabledFeaturesLogic) - const hasBranching = - featureFlags[FEATURE_FLAGS.SURVEYS_BRANCHING_LOGIC] && - (question.type === SurveyQuestionType.Rating || question.type === SurveyQuestionType.SingleChoice) + const hasBranching = featureFlags[FEATURE_FLAGS.SURVEYS_BRANCHING_LOGIC] const initialDescriptionContentType = descriptionContentType(index) ?? 'text' @@ -134,6 +132,7 @@ export function SurveyEditQuestionGroup({ index, question }: { index: number; qu editingDescription, editingThankYouMessage ) + resetBranchingForQuestion(index) }} options={[ { @@ -201,6 +200,7 @@ export function SurveyEditQuestionGroup({ index, question }: { index: number; qu const newQuestions = [...survey.questions] newQuestions[index] = newQuestion setSurveyValue('questions', newQuestions) + resetBranchingForQuestion(index) }} /> @@ -212,8 +212,17 @@ export function SurveyEditQuestionGroup({ index, question }: { index: number; qu label: '1 - 5', value: 5, }, - ...(question.display === 'number' ? [{ label: '0 - 10', value: 10 }] : []), + ...(question.display === 'number' + ? [{ label: '0 - 10 (Net Promoter Score)', value: 10 }] + : []), ]} + onChange={(val) => { + const newQuestion = { ...survey.questions[index], scale: val } + const newQuestions = [...survey.questions] + newQuestions[index] = newQuestion + setSurveyValue('questions', newQuestions) + resetBranchingForQuestion(index) + }} /> diff --git a/frontend/src/scenes/surveys/surveyLogic.test.ts b/frontend/src/scenes/surveys/surveyLogic.test.ts index 56e6615beb36f..4b12c4b1459f1 100644 --- a/frontend/src/scenes/surveys/surveyLogic.test.ts +++ b/frontend/src/scenes/surveys/surveyLogic.test.ts @@ -1,9 +1,9 @@ -import { expectLogic } from 'kea-test-utils' +import { expectLogic, partial } from 'kea-test-utils' import { surveyLogic } from 'scenes/surveys/surveyLogic' import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' -import { Survey, SurveyQuestionType, SurveyType } from '~/types' +import { Survey, SurveyQuestionBranchingType, SurveyQuestionType, SurveyType } from '~/types' const MULTIPLE_CHOICE_SURVEY: Survey = { id: '018b22a3-09b1-0000-2f5b-1bd8352ceec9', @@ -398,3 +398,439 @@ describe('single choice survey with open choice logic', () => { }) }) }) + +describe('set response-based survey branching', () => { + let logic: ReturnType + + beforeEach(() => { + initKeaTests() + logic = surveyLogic({ id: 'new' }) + logic.mount() + }) + + const SURVEY: Survey = { + id: '118b22a3-09b1-0000-2f5b-1bd8352ceec9', + name: 'My survey', + description: '', + type: SurveyType.Popover, + linked_flag: null, + linked_flag_id: null, + targeting_flag: null, + questions: [], + conditions: null, + appearance: { + position: 'right', + whiteLabel: false, + borderColor: '#c9c6c6', + placeholder: '', + backgroundColor: '#eeeded', + submitButtonText: 'Submit', + ratingButtonColor: 'white', + submitButtonColor: 'black', + thankYouMessageHeader: 'Thank you for your feedback!', + displayThankYouMessage: true, + ratingButtonActiveColor: 'black', + }, + created_at: '2023-10-12T06:46:32.113745Z', + created_by: { + id: 1, + uuid: '018aa8a6-10e8-0000-dba2-0e956f7bae38', + distinct_id: 'TGqg9Cn4jLkj9X87oXni9ZPBD6VbOxMtGV1GfJeB5LO', + first_name: 'test', + email: 'test@posthog.com', + is_email_verified: false, + }, + start_date: '2023-10-12T06:46:34.482000Z', + end_date: null, + archived: false, + targeting_flag_filters: undefined, + responses_limit: null, + } + + describe('main', () => { + // Single-choice question + it('set response-based branching for a single-choice question', async () => { + SURVEY.questions = [ + { + type: SurveyQuestionType.SingleChoice, + choices: ['Yes', 'No'], + question: 'Are you happy with our service?', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Glad to hear that. Tell us more!', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Sorry to hear that. Tell us more!', + description: '', + }, + ] + + await expectLogic(logic, () => { + logic.actions.loadSurveySuccess(SURVEY) + }).toDispatchActions(['loadSurveySuccess']) + + const questionIndex = 0 + + await expectLogic(logic, () => { + logic.actions.setQuestionBranchingType( + questionIndex, + SurveyQuestionBranchingType.ResponseBased, + undefined + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'Yes', + SurveyQuestionBranchingType.SpecificQuestion, + 1 + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'No', + SurveyQuestionBranchingType.SpecificQuestion, + 2 + ) + }) + .toDispatchActions([ + 'setQuestionBranchingType', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + ]) + .toMatchValues({ + survey: partial({ + questions: [ + { + ...SURVEY.questions[0], + branching: { + type: SurveyQuestionBranchingType.ResponseBased, + responseValues: { Yes: 1, No: 2 }, + }, + }, + { ...SURVEY.questions[1] }, + { ...SURVEY.questions[2] }, + ], + }), + }) + }) + + // Rating question, scale 1-3 + it('set response-based branching for a rating question with scale 3', async () => { + SURVEY.questions = [ + { + type: SurveyQuestionType.Rating, + question: 'How happy are you?', + description: '', + display: 'number', + scale: 3, + lowerBoundLabel: 'Unhappy', + upperBoundLabel: 'Happy', + buttonText: 'Submit', + }, + { + type: SurveyQuestionType.Open, + question: 'Sorry to hear that. Tell us more!', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Seems you are not completely happy. Tell us more!', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Glad to hear that. Tell us more!', + description: '', + }, + ] + + await expectLogic(logic, () => { + logic.actions.loadSurveySuccess(SURVEY) + }).toDispatchActions(['loadSurveySuccess']) + + const questionIndex = 0 + + await expectLogic(logic, () => { + logic.actions.setQuestionBranchingType( + questionIndex, + SurveyQuestionBranchingType.ResponseBased, + undefined + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'negative', + SurveyQuestionBranchingType.SpecificQuestion, + 1 + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'neutral', + SurveyQuestionBranchingType.SpecificQuestion, + 2 + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'positive', + SurveyQuestionBranchingType.SpecificQuestion, + 3 + ) + }) + .toDispatchActions([ + 'setQuestionBranchingType', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + ]) + .toMatchValues({ + survey: partial({ + questions: [ + { + ...SURVEY.questions[0], + branching: { + type: SurveyQuestionBranchingType.ResponseBased, + responseValues: { negative: 1, neutral: 2, positive: 3 }, + }, + }, + { ...SURVEY.questions[1] }, + { ...SURVEY.questions[2] }, + { ...SURVEY.questions[3] }, + ], + }), + }) + }) + + // Rating question, scale 1-5 + it('set response-based branching for a rating question with scale 5', async () => { + SURVEY.questions = [ + { + type: SurveyQuestionType.Rating, + question: 'How happy are you?', + description: '', + display: 'number', + scale: 5, + lowerBoundLabel: 'Unhappy', + upperBoundLabel: 'Happy', + buttonText: 'Submit', + }, + { + type: SurveyQuestionType.Open, + question: 'Sorry to hear that. Tell us more!', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Seems you are not completely happy. Tell us more!', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Glad to hear that. Tell us more!', + description: '', + }, + ] + + await expectLogic(logic, () => { + logic.actions.loadSurveySuccess(SURVEY) + }).toDispatchActions(['loadSurveySuccess']) + + const questionIndex = 0 + + await expectLogic(logic, () => { + logic.actions.setQuestionBranchingType( + questionIndex, + SurveyQuestionBranchingType.ResponseBased, + undefined + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'negative', + SurveyQuestionBranchingType.SpecificQuestion, + 1 + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'neutral', + SurveyQuestionBranchingType.SpecificQuestion, + 2 + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'positive', + SurveyQuestionBranchingType.SpecificQuestion, + 3 + ) + }) + .toDispatchActions([ + 'setQuestionBranchingType', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + ]) + .toMatchValues({ + survey: partial({ + questions: [ + { + ...SURVEY.questions[0], + branching: { + type: SurveyQuestionBranchingType.ResponseBased, + responseValues: { negative: 1, neutral: 2, positive: 3 }, + }, + }, + { ...SURVEY.questions[1] }, + { ...SURVEY.questions[2] }, + { ...SURVEY.questions[3] }, + ], + }), + }) + }) + + // Rating question, scale 0-10 (NPS) + it('set response-based branching for a rating question with scale 10', async () => { + SURVEY.questions = [ + { + type: SurveyQuestionType.Rating, + question: 'How happy are you?', + description: '', + display: 'number', + scale: 10, + lowerBoundLabel: 'Unhappy', + upperBoundLabel: 'Happy', + buttonText: 'Submit', + }, + { + type: SurveyQuestionType.Open, + question: 'Sorry to hear that. Tell us more!', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Seems you are not completely happy. Tell us more!', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Glad to hear that. Tell us more!', + description: '', + }, + ] + + await expectLogic(logic, () => { + logic.actions.loadSurveySuccess(SURVEY) + }).toDispatchActions(['loadSurveySuccess']) + + const questionIndex = 0 + + await expectLogic(logic, () => { + logic.actions.setQuestionBranchingType( + questionIndex, + SurveyQuestionBranchingType.ResponseBased, + undefined + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'detractors', + SurveyQuestionBranchingType.SpecificQuestion, + 1 + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'passives', + SurveyQuestionBranchingType.SpecificQuestion, + 2 + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 'promoters', + SurveyQuestionBranchingType.SpecificQuestion, + 3 + ) + }) + .toDispatchActions([ + 'setQuestionBranchingType', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + ]) + .toMatchValues({ + survey: partial({ + questions: [ + { + ...SURVEY.questions[0], + branching: { + type: SurveyQuestionBranchingType.ResponseBased, + responseValues: { detractors: 1, passives: 2, promoters: 3 }, + }, + }, + { ...SURVEY.questions[1] }, + { ...SURVEY.questions[2] }, + { ...SURVEY.questions[3] }, + ], + }), + }) + }) + + // Branch out to Next question / Confirmation message + it('branch out to next question or confirmation message', async () => { + SURVEY.questions = [ + { + type: SurveyQuestionType.SingleChoice, + choices: ['Yes', 'No'], + question: 'Are you happy with our service?', + description: '', + }, + { + type: SurveyQuestionType.Open, + question: 'Sorry to hear that. Tell us more!', + description: '', + }, + ] + + await expectLogic(logic, () => { + logic.actions.loadSurveySuccess(SURVEY) + }).toDispatchActions(['loadSurveySuccess']) + + const questionIndex = 0 + + await expectLogic(logic, () => { + logic.actions.setQuestionBranchingType( + questionIndex, + SurveyQuestionBranchingType.ResponseBased, + undefined + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 0, + SurveyQuestionBranchingType.ConfirmationMessage, + undefined + ) + logic.actions.setResponseBasedBranchingForQuestion( + questionIndex, + 1, + SurveyQuestionBranchingType.NextQuestion, + undefined + ) + }) + .toDispatchActions([ + 'setQuestionBranchingType', + 'setResponseBasedBranchingForQuestion', + 'setResponseBasedBranchingForQuestion', + ]) + .toMatchValues({ + survey: partial({ + questions: [ + { + ...SURVEY.questions[0], + branching: { + type: SurveyQuestionBranchingType.ResponseBased, + responseValues: { 0: SurveyQuestionBranchingType.ConfirmationMessage }, // Branching out to "Next question" is implicit + }, + }, + { ...SURVEY.questions[1] }, + ], + }), + }) + }) + }) +}) diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index 9b3964b64fdf3..b66a5326228a3 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -157,7 +157,18 @@ export const surveyLogic = kea([ isEditingDescription, isEditingThankYouMessage, }), - setQuestionBranching: (questionIndex, value) => ({ questionIndex, value }), + setQuestionBranchingType: (questionIndex, type, specificQuestionIndex) => ({ + questionIndex, + type, + specificQuestionIndex, + }), + setResponseBasedBranchingForQuestion: (questionIndex, responseValue, nextStep, specificQuestionIndex) => ({ + questionIndex, + responseValue, + nextStep, + specificQuestionIndex, + }), + resetBranchingForQuestion: (questionIndex) => ({ questionIndex }), archiveSurvey: true, setWritingHTMLDescription: (writingHTML: boolean) => ({ writingHTML }), setSurveyTemplateValues: (template: any) => ({ template }), @@ -661,38 +672,87 @@ export const surveyLogic = kea([ const newTemplateSurvey = { ...NEW_SURVEY, ...template } return newTemplateSurvey }, - setQuestionBranching: (state, { questionIndex, value }) => { + setQuestionBranchingType: (state, { questionIndex, type, specificQuestionIndex }) => { const newQuestions = [...state.questions] const question = newQuestions[questionIndex] - if ( - question.type !== SurveyQuestionType.Rating && - question.type !== SurveyQuestionType.SingleChoice - ) { - throw new Error( - `Survey question type must be ${SurveyQuestionType.Rating} or ${SurveyQuestionType.SingleChoice}` - ) - } - - if (value === SurveyQuestionBranchingType.NextQuestion) { + if (type === SurveyQuestionBranchingType.NextQuestion) { delete question.branching - } else if (value === SurveyQuestionBranchingType.ConfirmationMessage) { + } else if (type === SurveyQuestionBranchingType.ConfirmationMessage) { question.branching = { type: SurveyQuestionBranchingType.ConfirmationMessage, } - } else if (value === SurveyQuestionBranchingType.ResponseBased) { + } else if (type === SurveyQuestionBranchingType.ResponseBased) { + if ( + question.type !== SurveyQuestionType.Rating && + question.type !== SurveyQuestionType.SingleChoice + ) { + throw new Error( + `Survey question type must be ${SurveyQuestionType.Rating} or ${SurveyQuestionType.SingleChoice}` + ) + } + question.branching = { type: SurveyQuestionBranchingType.ResponseBased, - responseValue: {}, + responseValues: {}, } - } else if (value.startsWith(SurveyQuestionBranchingType.SpecificQuestion)) { - const nextQuestionIndex = parseInt(value.split(':')[1]) + } else if (type === SurveyQuestionBranchingType.SpecificQuestion) { question.branching = { type: SurveyQuestionBranchingType.SpecificQuestion, - index: nextQuestionIndex, + index: specificQuestionIndex, + } + } + + newQuestions[questionIndex] = question + return { + ...state, + questions: newQuestions, + } + }, + setResponseBasedBranchingForQuestion: ( + state, + { questionIndex, responseValue, nextStep, specificQuestionIndex } + ) => { + const newQuestions = [...state.questions] + const question = newQuestions[questionIndex] + + if ( + question.type !== SurveyQuestionType.Rating && + question.type !== SurveyQuestionType.SingleChoice + ) { + throw new Error( + `Survey question type must be ${SurveyQuestionType.Rating} or ${SurveyQuestionType.SingleChoice}` + ) + } + + if (question.branching?.type !== SurveyQuestionBranchingType.ResponseBased) { + throw new Error( + `Survey question branching type must be ${SurveyQuestionBranchingType.ResponseBased}` + ) + } + + if ('responseValues' in question.branching) { + if (nextStep === SurveyQuestionBranchingType.NextQuestion) { + delete question.branching.responseValues[responseValue] + } else if (nextStep === SurveyQuestionBranchingType.ConfirmationMessage) { + question.branching.responseValues[responseValue] = + SurveyQuestionBranchingType.ConfirmationMessage + } else if (nextStep === SurveyQuestionBranchingType.SpecificQuestion) { + question.branching.responseValues[responseValue] = specificQuestionIndex } } + newQuestions[questionIndex] = question + return { + ...state, + questions: newQuestions, + } + }, + resetBranchingForQuestion: (state, { questionIndex }) => { + const newQuestions = [...state.questions] + const question = newQuestions[questionIndex] + delete question.branching + newQuestions[questionIndex] = question return { ...state, @@ -943,6 +1003,32 @@ export const surveyLogic = kea([ return SurveyQuestionBranchingType.NextQuestion } + return SurveyQuestionBranchingType.ConfirmationMessage + }, + ], + getResponseBasedBranchingDropdownValue: [ + (s) => [s.survey], + (survey) => (questionIndex: number, question: RatingSurveyQuestion | MultipleSurveyQuestion, response) => { + if (!question.branching || !('responseValues' in question.branching)) { + return SurveyQuestionBranchingType.NextQuestion + } + + // If a value is mapped onto an integer, we're redirecting to a specific question + if (Number.isInteger(question.branching.responseValues[response])) { + const nextQuestionIndex = question.branching.responseValues[response] + return `${SurveyQuestionBranchingType.SpecificQuestion}:${nextQuestionIndex}` + } + + // If any other value is present (practically only Confirmation message), return that value + if (question.branching?.responseValues?.[response]) { + return question.branching.responseValues[response] + } + + // No branching specified, default to Next question / Confirmation message + if (questionIndex < survey.questions.length - 1) { + return SurveyQuestionBranchingType.NextQuestion + } + return SurveyQuestionBranchingType.ConfirmationMessage }, ], diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b91060b2e20be..e568b42145ce6 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -2660,6 +2660,11 @@ export interface SurveyQuestionBase { descriptionContentType?: SurveyQuestionDescriptionContentType optional?: boolean buttonText?: string + branching?: + | NextQuestionBranching + | ConfirmationMessageBranching + | ResponseBasedBranching + | SpecificQuestionBranching } export interface BasicSurveyQuestion extends SurveyQuestionBase { @@ -2723,7 +2728,7 @@ interface ConfirmationMessageBranching { interface ResponseBasedBranching { type: SurveyQuestionBranchingType.ResponseBased - responseValue: Record + responseValues: Record } interface SpecificQuestionBranching {