From 49cdd52be66581fe00a7b916004856d62fa3aea3 Mon Sep 17 00:00:00 2001 From: Soon-Mi Sugihara Date: Tue, 21 Nov 2023 13:16:23 -0500 Subject: [PATCH] Change multiple and single choice questions to have has_open_choice field --- .../src/scenes/surveys/SurveyAppearance.tsx | 7 +- frontend/src/scenes/surveys/SurveyEdit.tsx | 304 ++++++++++-------- frontend/src/scenes/surveys/constants.tsx | 1 - .../src/scenes/surveys/surveyLogic.test.ts | 202 +++++++++++- frontend/src/scenes/surveys/surveyLogic.tsx | 7 +- frontend/src/types.ts | 1 + 6 files changed, 379 insertions(+), 143 deletions(-) diff --git a/frontend/src/scenes/surveys/SurveyAppearance.tsx b/frontend/src/scenes/surveys/SurveyAppearance.tsx index d2793227e1acd..a049f10fd85bd 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyAppearance.tsx @@ -10,7 +10,7 @@ import { BasicSurveyQuestion, LinkSurveyQuestion, } from '~/types' -import { defaultSurveyAppearance, QUESTION_CHOICE_OPEN_ENDED_PREFIX } from './constants' +import { defaultSurveyAppearance } from './constants' import { cancel, check, @@ -643,12 +643,13 @@ export function SurveyMultipleChoiceAppearance({ )}
{(multipleChoiceQuestion.choices || []).map((choice, idx) => - choice.startsWith(QUESTION_CHOICE_OPEN_ENDED_PREFIX) ? ( + multipleChoiceQuestion?.has_open_choice && + idx === multipleChoiceQuestion.choices?.length - 1 ? ( ) : ( diff --git a/frontend/src/scenes/surveys/SurveyEdit.tsx b/frontend/src/scenes/surveys/SurveyEdit.tsx index 375665167723b..34eba11ca8668 100644 --- a/frontend/src/scenes/surveys/SurveyEdit.tsx +++ b/frontend/src/scenes/surveys/SurveyEdit.tsx @@ -39,7 +39,6 @@ import { defaultSurveyAppearance, SurveyQuestionLabel, SurveyUrlMatchTypeLabels, - QUESTION_CHOICE_OPEN_ENDED_PREFIX, } from './constants' import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' import React from 'react' @@ -450,139 +449,182 @@ export default function SurveyEdit(): JSX.Element { question.type === SurveyQuestionType.MultipleChoice) && (
- + {({ - value, - onChange, - }: { - value: string[] - onChange: (newValue: string[]) => void + value: hasOpenChoice, + onChange: toggleHasOpenChoice, }) => ( -
- {(value || []).map( - ( - choice: string, - index: number - ) => { - const isOpenEnded = - choice.startsWith( - QUESTION_CHOICE_OPEN_ENDED_PREFIX - ) - return ( -
- { - const newChoices = - [ - ...value, - ] - newChoices[ - index - ] = - (isOpenEnded - ? QUESTION_CHOICE_OPEN_ENDED_PREFIX - : '') + - val - onChange( - newChoices - ) - }} - /> - {isOpenEnded && ( - - open-ended - - )} - - } - size="small" - status="muted" - noPadding - onClick={() => { - const newChoices = - [ - ...value, - ] - newChoices.splice( - index, - 1 - ) - onChange( - newChoices - ) - }} - /> -
- ) - } + + {({ value, onChange }) => ( +
+ {(value || []).map( + ( + choice: string, + index: number + ) => { + const isOpenChoice = + hasOpenChoice && + index === + value?.length - + 1 + return ( +
+ { + const newChoices = + [ + ...value, + ] + newChoices[ + index + ] = + val + onChange( + newChoices + ) + }} + /> + {isOpenChoice && ( + + open-ended + + )} + + } + size="small" + status="muted" + noPadding + onClick={() => { + const newChoices = + [ + ...value, + ] + newChoices.splice( + index, + 1 + ) + onChange( + newChoices + ) + if ( + isOpenChoice + ) { + toggleHasOpenChoice( + false + ) + } + }} + /> +
+ ) + } + )} +
+ {(value || []).length < + 6 && ( + <> + + } + type="secondary" + fullWidth={ + false + } + onClick={() => { + if ( + !value + ) { + onChange( + [ + '', + ] + ) + } else if ( + hasOpenChoice + ) { + const newChoices = + value.slice( + 0, + -1 + ) + newChoices.push( + '' + ) + newChoices.push( + value[ + value.length - + 1 + ] + ) + onChange( + newChoices + ) + } else { + onChange( + [ + ...value, + '', + ] + ) + } + }} + > + Add choice + + {!hasOpenChoice && ( + + } + type="secondary" + fullWidth={ + false + } + onClick={() => { + if ( + !value + ) { + onChange( + [ + 'Other', + ] + ) + } else { + onChange( + [ + ...value, + 'Other', + ] + ) + } + toggleHasOpenChoice( + true + ) + }} + > + Add + open-ended + choice + + )} + + )} +
+
)} -
- {(value || []).length < 6 && ( - <> - - } - type="secondary" - fullWidth={false} - onClick={() => { - if (!value) { - onChange([ - '', - ]) - } else { - onChange([ - ...value, - '', - ]) - } - }} - > - Add choice - - - } - type="secondary" - fullWidth={false} - onClick={() => { - if (!value) { - onChange([ - QUESTION_CHOICE_OPEN_ENDED_PREFIX + - 'Other', - ]) - } else { - onChange([ - ...value, - QUESTION_CHOICE_OPEN_ENDED_PREFIX + - 'Other', - ]) - } - }} - > - Add open-ended - choice - - - )} -
-
+
)}
diff --git a/frontend/src/scenes/surveys/constants.tsx b/frontend/src/scenes/surveys/constants.tsx index 69d3349e10513..b63a8ee82a2a9 100644 --- a/frontend/src/scenes/surveys/constants.tsx +++ b/frontend/src/scenes/surveys/constants.tsx @@ -2,7 +2,6 @@ import { FeatureFlagFilters, Survey, SurveyQuestionType, SurveyType, SurveyUrlMa export const SURVEY_EVENT_NAME = 'survey sent' export const SURVEY_RESPONSE_PROPERTY = '$survey_response' -export const QUESTION_CHOICE_OPEN_ENDED_PREFIX = 'OPENlabel=' export const SurveyQuestionLabel = { [SurveyQuestionType.Open]: 'Freeform text', diff --git a/frontend/src/scenes/surveys/surveyLogic.test.ts b/frontend/src/scenes/surveys/surveyLogic.test.ts index f2d39471cd392..a7a3a5c529e79 100644 --- a/frontend/src/scenes/surveys/surveyLogic.test.ts +++ b/frontend/src/scenes/surveys/surveyLogic.test.ts @@ -15,7 +15,7 @@ const MULTIPLE_CHOICE_SURVEY: Survey = { questions: [ { type: SurveyQuestionType.MultipleChoice, - choices: ['Tutorials', 'Customer case studies', 'Product announcements', 'OPENlabel=Other'], + choices: ['Tutorials', 'Customer case studies', 'Product announcements', 'Other'], question: 'Which types of content would you like to see more of?', description: '', }, @@ -60,7 +60,7 @@ const SINGLE_CHOICE_SURVEY: Survey = { questions: [ { type: SurveyQuestionType.SingleChoice, - choices: ['Yes', 'No', 'OPENlabel=Maybe (explain)'], + choices: ['Yes', 'No'], question: 'Would you like us to continue this feature?', description: '', }, @@ -94,6 +94,98 @@ const SINGLE_CHOICE_SURVEY: Survey = { targeting_flag_filters: undefined, } +const MULTIPLE_CHOICE_SURVEY_WITH_OPEN_CHOICE: Survey = { + id: '018b22a3-09b1-0000-2f5b-1bd8352ceec9', + name: 'Multiple Choice survey', + description: '', + type: SurveyType.Popover, + linked_flag: null, + linked_flag_id: null, + targeting_flag: null, + questions: [ + { + type: SurveyQuestionType.MultipleChoice, + choices: ['Tutorials', 'Customer case studies', 'Product announcements', 'Other'], + question: 'Which types of content would you like to see more of?', + description: '', + has_open_choice: true, + }, + ], + 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, +} + +const SINGLE_CHOICE_SURVEY_WITH_OPEN_CHOICE: Survey = { + id: '118b22a3-09b1-0000-2f5b-1bd8352ceec9', + name: 'Single Choice survey', + description: '', + type: SurveyType.Popover, + linked_flag: null, + linked_flag_id: null, + targeting_flag: null, + questions: [ + { + type: SurveyQuestionType.SingleChoice, + choices: ['Yes', 'No', 'Maybe (explain)'], + question: 'Would you like us to continue this feature?', + description: '', + has_open_choice: true, + }, + ], + 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, +} + describe('multiple choice survey logic', () => { let logic: ReturnType @@ -114,7 +206,7 @@ describe('multiple choice survey logic', () => { results: [ [336, '"Tutorials"'], [312, '"Customer case studies"'], - [20, '"Other choice"'], + [20, '"Other"'], ], }, ], @@ -135,7 +227,7 @@ describe('multiple choice survey logic', () => { .toMatchValues({ surveyMultipleChoiceResults: { 0: { - labels: ['Tutorials', 'Customer case studies', 'Other choice', 'Product announcements'], + labels: ['Tutorials', 'Customer case studies', 'Other', 'Product announcements'], data: [336, 312, 20, 0], }, }, @@ -163,7 +255,7 @@ describe('single choice survey logic', () => { { results: [ ['"Yes"', 101], - ['"Only if I could customize my choices"', 1], + ['"No"', 1], ], }, ], @@ -177,6 +269,106 @@ describe('single choice survey logic', () => { logic.actions.loadSurveySuccess(SINGLE_CHOICE_SURVEY) }).toDispatchActions(['loadSurveySuccess']) + await expectLogic(logic, () => { + logic.actions.loadSurveySingleChoiceResults({ questionIndex: 1 }) + }) + .toFinishAllListeners() + .toMatchValues({ + surveySingleChoiceResults: { + 1: { + labels: ['"Yes"', '"No"'], + data: [101, 1], + total: 102, + }, + }, + }) + }) + }) +}) + +describe('multiple choice survey with open choice logic', () => { + let logic: ReturnType + + beforeEach(() => { + initKeaTests() + logic = surveyLogic({ id: 'new' }) + logic.mount() + + useMocks({ + get: { + '/api/projects/:team/surveys/': () => [200, { results: [] }], + '/api/projects/:team/surveys/responses_count': () => [200, {}], + }, + post: { + '/api/projects/:team/query/': () => [ + 200, + { + results: [ + [336, '"Tutorials"'], + [312, '"Customer case studies"'], + [20, '"Other choice"'], + ], + }, + ], + }, + }) + }) + + describe('main', () => { + it('post processes return values', async () => { + await expectLogic(logic, () => { + logic.actions.loadSurveySuccess(MULTIPLE_CHOICE_SURVEY_WITH_OPEN_CHOICE) + }).toDispatchActions(['loadSurveySuccess']) + + await expectLogic(logic, () => { + logic.actions.loadSurveyMultipleChoiceResults({ questionIndex: 0 }) + }) + .toFinishAllListeners() + .toMatchValues({ + surveyMultipleChoiceResults: { + 0: { + labels: ['Tutorials', 'Customer case studies', 'Other choice', 'Product announcements'], + data: [336, 312, 20, 0], + }, + }, + }) + }) + }) +}) + +describe('single choice survey with open choice logic', () => { + let logic: ReturnType + + beforeEach(() => { + initKeaTests() + logic = surveyLogic({ id: 'new' }) + logic.mount() + + useMocks({ + get: { + '/api/projects/:team/surveys/': () => [200, { results: [] }], + '/api/projects/:team/surveys/responses_count': () => [200, {}], + }, + post: { + '/api/projects/:team/query/': () => [ + 200, + { + results: [ + ['"Yes"', 101], + ['"Only if I could customize my choices"', 1], + ], + }, + ], + }, + }) + }) + + describe('main', () => { + it('post processes return values', async () => { + await expectLogic(logic, () => { + logic.actions.loadSurveySuccess(SINGLE_CHOICE_SURVEY_WITH_OPEN_CHOICE) + }).toDispatchActions(['loadSurveySuccess']) + await expectLogic(logic, () => { logic.actions.loadSurveySingleChoiceResults({ questionIndex: 1 }) }) diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index 1af7a187761a8..654e28cdf04c8 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -22,7 +22,7 @@ import { dayjs } from 'lib/dayjs' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' import { featureFlagLogic as enabledFlagLogic } from 'lib/logic/featureFlagLogic' -import { defaultSurveyFieldValues, QUESTION_CHOICE_OPEN_ENDED_PREFIX, NEW_SURVEY, NewSurvey } from './constants' +import { defaultSurveyFieldValues, NEW_SURVEY, NewSurvey } from './constants' import { sanitizeHTML } from './utils' import { Scene } from 'scenes/sceneTypes' @@ -335,10 +335,11 @@ export const surveyLogic = kea([ }) // Zero-fill choices that are not open-ended - question.choices.forEach((choice) => { + question.choices.forEach((choice, idx) => { if ( results?.length && - !choice.startsWith(QUESTION_CHOICE_OPEN_ENDED_PREFIX) && + idx === question.choices.length - 1 && + question?.has_open_choice && !results.some((r) => r[1] === choice) ) { results.push([0, choice]) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1273d98ef07e7..b8f9c79bd6e33 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -2241,6 +2241,7 @@ export interface RatingSurveyQuestion extends SurveyQuestionBase { export interface MultipleSurveyQuestion extends SurveyQuestionBase { type: SurveyQuestionType.SingleChoice | SurveyQuestionType.MultipleChoice choices: string[] + has_open_choice?: boolean } export type SurveyQuestion = BasicSurveyQuestion | LinkSurveyQuestion | RatingSurveyQuestion | MultipleSurveyQuestion