diff --git a/cypress/e2e/surveys.cy.ts b/cypress/e2e/surveys.cy.ts index ad6e45ff3201a..352a97d8e85de 100644 --- a/cypress/e2e/surveys.cy.ts +++ b/cypress/e2e/surveys.cy.ts @@ -114,6 +114,9 @@ describe('Surveys', () => { cy.get('[data-attr="survey-preview"]').find('form').find('.ratings-number').should('have.length', 5) // add targeting filters + cy.get('.LemonCollapsePanel').contains('Targeting').click() + cy.contains('All users').click() + cy.get('.Popover__content').contains('Users who match').click() cy.contains('Add user targeting').click() // select the first property @@ -156,6 +159,7 @@ describe('Surveys', () => { cy.get('.Popover__content').contains('Edit').click() // remove user targeting properties + cy.get('.LemonCollapsePanel').contains('Targeting').click() cy.contains('Remove all user properties').click() // save diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f3de742cdb817..b2ce0bb71285c 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -30,6 +30,7 @@ beforeEach(() => { decideResponse({ // set feature flags here e.g. // 'toolbar-launch-side-action': true, + 'surveys-new-creation-flow': true, 'surveys-results-visualizations': true, 'auto-redirect': true, notebooks: true, diff --git a/ee/clickhouse/views/test/test_clickhouse_experiments.py b/ee/clickhouse/views/test/test_clickhouse_experiments.py index 23cde87e344e1..7b1d70c046ee3 100644 --- a/ee/clickhouse/views/test/test_clickhouse_experiments.py +++ b/ee/clickhouse/views/test/test_clickhouse_experiments.py @@ -119,7 +119,6 @@ def test_creating_updating_basic_experiment(self): self.assertEqual(experiment.end_date.strftime("%Y-%m-%dT%H:%M"), end_date) def test_adding_behavioral_cohort_filter_to_experiment_fails(self): - cohort = Cohort.objects.create( team=self.team, filters={ @@ -739,7 +738,6 @@ def test_creating_experiment_with_group_aggregation_parameter(self): self.assertEqual(created_ff.filters["aggregation_group_type_index"], 0) def test_used_in_experiment_is_populated_correctly_for_feature_flag_list(self) -> None: - ff_key = "a-b-test" response = self.client.post( f"/api/projects/{self.team.id}/experiments/", @@ -945,7 +943,6 @@ def test_create_experiment_updates_feature_flag_cache(self): class ClickhouseTestFunnelExperimentResults(ClickhouseTestMixin, APILicensedTest): @snapshot_clickhouse_queries def test_experiment_flow_with_event_results(self): - journeys_for( { "person1": [ @@ -1031,7 +1028,6 @@ def test_experiment_flow_with_event_results(self): @snapshot_clickhouse_queries def test_experiment_flow_with_event_results_with_hogql_aggregation(self): - journeys_for( { "person1": [ @@ -1150,7 +1146,6 @@ def test_experiment_flow_with_event_results_with_hogql_aggregation(self): self.assertAlmostEqual(response_data["expected_loss"], 1, places=2) def test_experiment_flow_with_event_results_cached(self): - journeys_for( { "person1": [ @@ -1248,7 +1243,6 @@ def test_experiment_flow_with_event_results_cached(self): @snapshot_clickhouse_queries def test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones(self): - journeys_for( { "person1": [ diff --git a/ee/session_recordings/session_recording_extensions.py b/ee/session_recordings/session_recording_extensions.py index ebfb3896a1415..0a7ec6233bea8 100644 --- a/ee/session_recordings/session_recording_extensions.py +++ b/ee/session_recordings/session_recording_extensions.py @@ -25,6 +25,7 @@ MINIMUM_AGE_FOR_RECORDING = timedelta(hours=24) + # TODO rename this... def save_recording_with_new_content(recording: SessionRecording, content: str) -> str: if not settings.OBJECT_STORAGE_ENABLED: diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey.png b/frontend/__snapshots__/scenes-app-surveys--new-survey.png index 95162d2315a21..85aeba5db767d 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey.png differ diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 52e67304318f0..fd25cde2002a9 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -162,6 +162,7 @@ export const FEATURE_FLAGS = { WEBHOOKS_DENYLIST: 'webhooks-denylist', // owner: #team-pipeline SURVEYS_MULTIPLE_QUESTIONS: 'surveys-multiple-questions', // owner: @liyiy SURVEYS_RESULTS_VISUALIZATIONS: 'surveys-results-visualizations', // owner: @jurajmajerik + SURVEYS_NEW_CREATION_FLOW: 'surveys-new-creation-flow', // owner: @liyiy CONSOLE_RECORDING_SEARCH: 'console-recording-search', // owner: #team-monitoring PERSONS_HOGQL_QUERY: 'persons-hogql-query', // owner: @mariusandra } as const diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx index 0899fb415c3f6..8ebfa1be59f98 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx @@ -88,8 +88,6 @@ const Component = ({ attributes }: NotebookNodeProps {}} /> diff --git a/frontend/src/scenes/surveys/EditSurvey.scss b/frontend/src/scenes/surveys/EditSurvey.scss new file mode 100644 index 0000000000000..2995d06667887 --- /dev/null +++ b/frontend/src/scenes/surveys/EditSurvey.scss @@ -0,0 +1,9 @@ +.presentation-preview .CodeSnippet__actions { + display: none; +} + +.SurveyForm { + .LemonCollapsePanel__header { + background: var(--border-light); + } +} diff --git a/frontend/src/scenes/surveys/EditSurveyNew.tsx b/frontend/src/scenes/surveys/EditSurveyNew.tsx new file mode 100644 index 0000000000000..10d32135757ae --- /dev/null +++ b/frontend/src/scenes/surveys/EditSurveyNew.tsx @@ -0,0 +1,969 @@ +import './EditSurvey.scss' +import { surveyLogic } from './surveyLogic' +import { BindLogic, useActions, useValues } from 'kea' +import { Group } from 'kea-forms' +import { + LemonBanner, + LemonButton, + LemonCheckbox, + LemonCollapse, + LemonDivider, + LemonInput, + LemonSelect, + LemonTabs, + LemonTextArea, +} from '@posthog/lemon-ui' +import { Field, PureField } from 'lib/forms/Field' +import { + SurveyQuestion, + SurveyQuestionType, + SurveyType, + LinkSurveyQuestion, + RatingSurveyQuestion, + SurveyUrlMatchType, +} from '~/types' +import { FlagSelector } from 'scenes/early-access-features/EarlyAccessFeature' +import { IconCancel, IconDelete, IconPlus, IconPlusMini } from 'lib/lemon-ui/icons' +import { + BaseAppearance, + Customization, + SurveyAppearance, + SurveyMultipleChoiceAppearance, + SurveyRatingAppearance, +} from './SurveyAppearance' +import { SurveyAPIEditor } from './SurveyAPIEditor' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' +import { + defaultSurveyFieldValues, + defaultSurveyAppearance, + SurveyQuestionLabel, + SurveyUrlMatchTypeLabels, +} from './constants' +import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' +import React, { useState } from 'react' +import { CodeEditor } from 'lib/components/CodeEditors' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' +import { SurveyFormAppearance } from './SurveyFormAppearance' + +function PresentationTypeCard({ + title, + description, + children, + onClick, + value, + active, +}: { + title: string + description?: string + children: React.ReactNode + onClick: () => void + value: any + active: boolean +}): JSX.Element { + return ( +
+

{title}

+ {description &&

{description}

} +
{children}
+ +
+ ) +} + +export default function EditSurveyNew(): JSX.Element { + const { survey, hasTargetingFlag, urlMatchTypeValidationError, writingHTMLDescription, hasTargetingSet } = + useValues(surveyLogic) + const { setSurveyValue, setDefaultForQuestionType, setWritingHTMLDescription, resetTargeting } = + useActions(surveyLogic) + const { featureFlags } = useValues(enabledFeaturesLogic) + + const [activePreview, setActivePreview] = useState(0) + + const showThankYou = survey.appearance.displayThankYouMessage && activePreview >= survey.questions.length + + return ( +
+
+ + + + + + + + setActivePreview(index || 0)} + panels={[ + ...survey.questions.map( + ( + question: + | LinkSurveyQuestion + | SurveyQuestion + | RatingSurveyQuestion, + index: number + ) => ({ + key: index, + header: ( +
+ + Question {index + 1}. {question.question} + + {survey.questions.length > 1 && ( + } + status="primary-alt" + data-attr={`delete-survey-question-${index}`} + onClick={(e) => { + e.stopPropagation() + setActivePreview(index <= 0 ? 0 : index - 1) + setSurveyValue( + 'questions', + survey.questions.filter( + (_, i) => i !== index + ) + ) + }} + tooltipPlacement="topRight" + /> + )} +
+ ), + content: ( + +
+ + + + + + {({ value, onChange }) => ( + <> + + setWritingHTMLDescription( + key === 'html' + ) + } + tabs={[ + { + key: 'text', + label: ( + + Text + + ), + content: ( + + onChange(v) + } + /> + ), + }, + { + key: 'html', + label: ( + + HTML + + ), + content: ( +
+ + onChange( + v ?? '' + ) + } + height={150} + options={{ + minimap: { + enabled: + false, + }, + wordWrap: 'on', + scrollBeyondLastLine: + false, + automaticLayout: + true, + fixedOverflowWidgets: + true, + lineNumbers: + 'off', + glyphMargin: + false, + folding: false, + }} + /> +
+ ), + }, + ]} + /> + {question.description && + question.description + ?.toLowerCase() + .includes(' + Scripts won't run in the survey + popup and we'll remove these on + save. Use the API question mode + to run your own scripts in + surveys. + + )} + + )} +
+ + { + const isEditingQuestion = + defaultSurveyFieldValues[question.type] + .questions[0].question !== + question.question + const isEditingDescription = + defaultSurveyFieldValues[question.type] + .questions[0].description !== + question.description + const isEditingThankYouMessage = + defaultSurveyFieldValues[question.type] + .appearance + .thankYouMessageHeader !== + survey.appearance.thankYouMessageHeader + setDefaultForQuestionType( + index, + newType, + isEditingQuestion, + isEditingDescription, + isEditingThankYouMessage + ) + }} + options={[ + { + label: SurveyQuestionLabel[ + SurveyQuestionType.Open + ], + value: SurveyQuestionType.Open, + tooltip: () => ( + undefined} + appearance={{ + ...survey.appearance, + whiteLabel: true, + }} + question="Share your thoughts" + description="Optional form description." + type={SurveyQuestionType.Open} + /> + ), + }, + { + label: 'Link', + value: SurveyQuestionType.Link, + tooltip: () => ( + undefined} + appearance={{ + ...survey.appearance, + whiteLabel: true, + submitButtonText: + 'Register', + }} + question="Do you want to join our upcoming webinar?" + type={SurveyQuestionType.Link} + /> + ), + }, + { + label: 'Rating', + value: SurveyQuestionType.Rating, + tooltip: () => ( + undefined} + appearance={{ + ...survey.appearance, + whiteLabel: true, + }} + question="How satisfied are you with our product?" + description="Optional form description." + ratingSurveyQuestion={{ + display: 'number', + lowerBoundLabel: + 'Not great', + upperBoundLabel: + 'Fantastic', + question: + 'How satisfied are you with our product?', + scale: 5, + type: SurveyQuestionType.Rating, + }} + /> + ), + }, + ...[ + { + label: 'Single choice select', + value: SurveyQuestionType.SingleChoice, + tooltip: () => ( + undefined} + appearance={{ + ...survey.appearance, + whiteLabel: true, + }} + question="Have you found this tutorial useful?" + multipleChoiceQuestion={{ + type: SurveyQuestionType.SingleChoice, + choices: ['Yes', 'No'], + question: + 'Have you found this tutorial useful?', + }} + /> + ), + }, + { + label: 'Multiple choice select', + value: SurveyQuestionType.MultipleChoice, + tooltip: () => ( + undefined} + appearance={{ + ...survey.appearance, + whiteLabel: true, + }} + question="Which types of content would you like to see more of?" + multipleChoiceQuestion={{ + type: SurveyQuestionType.MultipleChoice, + choices: [ + 'Tutorials', + 'Customer case studies', + 'Product announcements', + ], + question: + 'Which types of content would you like to see more of?', + }} + /> + ), + }, + ], + ]} + /> + + {survey.questions.length > 1 && ( + + + + )} + {question.type === SurveyQuestionType.Link && ( + + + + )} + {question.type === SurveyQuestionType.Rating && ( +
+
+ + + + + + +
+
+ + + + + + +
+
+ )} + {(question.type === SurveyQuestionType.SingleChoice || + question.type === + SurveyQuestionType.MultipleChoice) && ( +
+ + {({ value, onChange }) => ( +
+ {(value || []).map( + ( + choice: string, + index: number + ) => ( +
+ { + const newChoices = + [...value] + newChoices[ + index + ] = val + onChange( + newChoices + ) + }} + /> + + } + size="small" + status="muted" + noPadding + onClick={() => { + const newChoices = + [...value] + newChoices.splice( + index, + 1 + ) + onChange( + newChoices + ) + }} + /> +
+ ) + )} +
+ {(value || []).length < 6 && ( + } + type="secondary" + fullWidth={false} + onClick={() => { + if (!value) { + onChange(['']) + } else { + onChange([ + ...value, + '', + ]) + } + }} + > + Add choice + + )} +
+
+ )} +
+
+ )} +
+
+ ), + }) + ), + ...(survey.appearance.displayThankYouMessage + ? [ + { + key: survey.questions.length, + header: ( +
+ Confirmation message + } + status="primary-alt" + data-attr={`delete-survey-confirmation`} + onClick={(e) => { + e.stopPropagation() + setActivePreview(survey.questions.length - 1) + setSurveyValue('appearance', { + ...survey.appearance, + displayThankYouMessage: false, + }) + }} + tooltipPlacement="topRight" + /> +
+ ), + content: ( + <> + + + setSurveyValue('appearance', { + ...survey.appearance, + thankYouMessageHeader: val, + }) + } + placeholder="ex: Thank you for your feedback!" + /> + + + + setSurveyValue('appearance', { + ...survey.appearance, + thankYouMessageDescription: val, + }) + } + minRows={2} + placeholder="ex: We really appreciate it." + /> + + + ), + }, + ] + : []), + ]} + /> +
+ {featureFlags[FEATURE_FLAGS.SURVEYS_MULTIPLE_QUESTIONS] && ( + // TODO: Add pay gate mini here once billing is resolved for it + } + onClick={() => { + setSurveyValue('questions', [ + ...survey.questions, + { ...defaultSurveyFieldValues.open.questions[0] }, + ]) + setActivePreview(survey.questions.length) + }} + > + Add question + + )} + {!survey.appearance.displayThankYouMessage && ( + } + onClick={() => { + setSurveyValue('appearance', { + ...survey.appearance, + displayThankYouMessage: true, + }) + setActivePreview(survey.questions.length) + }} + > + Add confirmation message + + )} +
+ + ), + }, + { + key: 'presentation', + header: 'Presentation', + content: ( + + {({ onChange, value }) => { + return ( +
+ onChange(SurveyType.Popover)} + title="Popover" + description="Automatically appears when PostHog JS is installed" + value={SurveyType.Popover} + > +
+ 1 + ? { submitButtonText: 'Next' } + : null), + }} + /> +
+
+ onChange(SurveyType.API)} + title="API" + description="Use the PostHog API to show/hide your survey programmatically" + value={SurveyType.API} + > +
+ +
+
+
+ ) + }} +
+ ), + }, + ...(survey.type !== SurveyType.API + ? [ + { + key: 'customization', + header: 'Customization', + content: ( + + {({ value, onChange }) => ( + { + onChange(appearance) + }} + /> + )} + + ), + }, + ] + : []), + { + key: 'targeting', + header: 'Targeting', + content: ( + + { + if (value) { + resetTargeting() + } else { + // TRICKY: When attempting to set user match conditions + // we want a proxy value to be set so that the user + // can then edit these, or decide to go back to all user targeting + setSurveyValue('conditions', { url: '' }) + } + }} + value={!hasTargetingSet} + options={[ + { label: 'All users', value: true }, + { label: 'Users who match...', value: false }, + ]} + /> + {!hasTargetingSet ? ( + + Survey will be released to everyone + + ) : ( + <> + + Connecting to a feature flag will automatically enable this + survey for everyone in the feature flag. + + } + > + {({ value, onChange }) => ( +
+ + {value && ( + } + size="small" + status="stealth" + onClick={() => onChange(null)} + aria-label="close" + /> + )} +
+ )} +
+ + {({ value, onChange }) => ( + <> + +
+ URL + { + onChange({ + ...value, + urlMatchType: matchTypeVal, + }) + }} + data-attr="survey-url-matching-type" + options={Object.keys(SurveyUrlMatchTypeLabels).map( + (key) => ({ + label: SurveyUrlMatchTypeLabels[key], + value: key, + }) + )} + /> + + onChange({ ...value, url: urlVal }) + } + placeholder="ex: https://app.posthog.com" + fullWidth + /> +
+
+ + + onChange({ ...value, selector: selectorVal }) + } + placeholder="ex: .className or #id" + /> + + +
+ { + if (checked) { + onChange({ + ...value, + seenSurveyWaitPeriodInDays: + value?.seenSurveyWaitPeriodInDays || + 30, + }) + } else { + const { + seenSurveyWaitPeriodInDays, + ...rest + } = value || {} + onChange(rest) + } + }} + /> + Do not display this survey to users who have already + seen a survey in the last + { + if (val !== undefined && val > 0) { + onChange({ + ...value, + seenSurveyWaitPeriodInDays: val, + }) + } + }} + className="w-16" + />{' '} + days. +
+
+ + )} +
+ + + {!hasTargetingFlag && ( + { + setSurveyValue('targeting_flag_filters', { groups: [] }) + setSurveyValue('remove_targeting_flag', false) + }} + > + Add user targeting + + )} + {hasTargetingFlag && ( + <> +
+ +
+ { + setSurveyValue('targeting_flag_filters', null) + setSurveyValue('targeting_flag', null) + setSurveyValue('remove_targeting_flag', true) + }} + > + Remove all user properties + + + )} +
+
+ + )} +
+ ), + }, + ]} + /> +
+ +
+ setActivePreview(preview)} + /> +
+
+ ) +} diff --git a/frontend/src/scenes/surveys/EditSurveyOld.tsx b/frontend/src/scenes/surveys/EditSurveyOld.tsx new file mode 100644 index 0000000000000..45c0178c417a7 --- /dev/null +++ b/frontend/src/scenes/surveys/EditSurveyOld.tsx @@ -0,0 +1,547 @@ +import { surveyLogic } from './surveyLogic' +import { BindLogic, useActions, useValues } from 'kea' +import { Group } from 'kea-forms' +import { + LemonBanner, + LemonButton, + LemonCheckbox, + LemonCollapse, + LemonDivider, + LemonInput, + LemonSelect, + LemonTabs, + LemonTextArea, +} from '@posthog/lemon-ui' +import { Field, PureField } from 'lib/forms/Field' +import { + SurveyQuestion, + SurveyQuestionType, + SurveyType, + LinkSurveyQuestion, + RatingSurveyQuestion, + SurveyUrlMatchType, +} from '~/types' +import { FlagSelector } from 'scenes/early-access-features/EarlyAccessFeature' +import { IconCancel, IconDelete, IconPlus, IconPlusMini } from 'lib/lemon-ui/icons' +import { Customization, SurveyAppearance } from './SurveyAppearance' +import { SurveyAPIEditor } from './SurveyAPIEditor' +import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' +import { defaultSurveyFieldValues, defaultSurveyAppearance, SurveyUrlMatchTypeLabels } from './constants' +import { FEATURE_FLAGS } from 'lib/constants' +import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' +import { CodeEditor } from 'lib/components/CodeEditors' + +export default function EditSurveyOld(): JSX.Element { + const { survey, hasTargetingFlag, urlMatchTypeValidationError, writingHTMLDescription } = useValues(surveyLogic) + const { setSurveyValue, setDefaultForQuestionType, setWritingHTMLDescription } = useActions(surveyLogic) + const { featureFlags } = useValues(enabledFeaturesLogic) + + return ( +
+
+ + + + + + + + + + +
Questions
+ {survey.questions.map( + (question: LinkSurveyQuestion | SurveyQuestion | RatingSurveyQuestion, index: number) => ( + + + {question.question} + {survey.questions.length > 1 && ( + } + status="primary-alt" + data-attr={`delete-survey-question-${index}`} + onClick={() => { + setSurveyValue( + 'questions', + survey.questions.filter((_, i) => i !== index) + ) + }} + tooltipPlacement="topRight" + /> + )} +
+ ), + content: ( +
+ + { + const isEditingQuestion = + defaultSurveyFieldValues[question.type].questions[0] + .question !== question.question + const isEditingDescription = + defaultSurveyFieldValues[question.type].questions[0] + .description !== question.description + const isEditingThankYouMessage = + defaultSurveyFieldValues[question.type].appearance + .thankYouMessageHeader !== + survey.appearance.thankYouMessageHeader + setDefaultForQuestionType( + index, + newType, + isEditingQuestion, + isEditingDescription, + isEditingThankYouMessage + ) + }} + options={[ + { label: 'Open text', value: SurveyQuestionType.Open }, + { label: 'Link', value: SurveyQuestionType.Link }, + { label: 'Rating', value: SurveyQuestionType.Rating }, + ...[ + { + label: 'Single choice select', + value: SurveyQuestionType.SingleChoice, + }, + { + label: 'Multiple choice select', + value: SurveyQuestionType.MultipleChoice, + }, + ], + ]} + /> + + {survey.questions.length > 1 && ( + + + + )} + + + + {question.type === SurveyQuestionType.Link && ( + + + + )} + + {({ value, onChange }) => ( + <> + + setWritingHTMLDescription(key === 'html') + } + tabs={[ + { + key: 'text', + label: Text, + content: ( + onChange(v)} + /> + ), + }, + { + key: 'html', + label: HTML, + content: ( +
+ onChange(v ?? '')} + height={150} + options={{ + minimap: { enabled: false }, + wordWrap: 'on', + scrollBeyondLastLine: false, + automaticLayout: true, + fixedOverflowWidgets: true, + lineNumbers: 'off', + glyphMargin: false, + folding: false, + }} + /> +
+ ), + }, + ]} + /> + {question.description && + question.description + ?.toLowerCase() + .includes(' + Scripts won't run in the survey popup and we'll + remove these on save. Use the API question mode + to run your own scripts in surveys. + + )} + + )} +
+ {question.type === SurveyQuestionType.Rating && ( +
+
+ + + + + + +
+
+ + + + + + +
+
+ )} + {(question.type === SurveyQuestionType.SingleChoice || + question.type === SurveyQuestionType.MultipleChoice) && ( +
+ + {({ value, onChange }) => ( +
+ {(value || []).map( + (choice: string, index: number) => ( +
+ { + const newChoices = [...value] + newChoices[index] = val + onChange(newChoices) + }} + /> + } + size="small" + status="muted" + noPadding + onClick={() => { + const newChoices = [...value] + newChoices.splice(index, 1) + onChange(newChoices) + }} + /> +
+ ) + )} +
+ {(value || []).length < 6 && ( + } + type="secondary" + fullWidth={false} + onClick={() => { + if (!value) { + onChange(['']) + } else { + onChange([...value, '']) + } + }} + > + Add choice + + )} +
+
+ )} +
+
+ )} +
+ ), + }, + ]} + /> + + ) + )} + {featureFlags[FEATURE_FLAGS.SURVEYS_MULTIPLE_QUESTIONS] && ( + // TODO: Add pay gate mini here once billing is resolved for it + } + onClick={() => { + setSurveyValue('questions', [ + ...survey.questions, + { ...defaultSurveyFieldValues.open.questions[0] }, + ]) + }} + > + Add question + + )} + + + {({ value, onChange }) => ( + <> + onChange({ ...value, displayThankYouMessage: checked })} + /> + {value.displayThankYouMessage && ( + <> + + onChange({ ...value, thankYouMessageHeader: val })} + placeholder="ex: Thank you for your feedback!" + /> + + + onChange({ ...value, thankYouMessageDescription: val })} + minRows={2} + placeholder="ex: We really appreciate it." + /> + + + )} + + )} + + + + + If targeting options are set, the survey will be released to users who match all of the + conditions. If no targeting options are set, the survey will be released to everyone. + + + Connecting to a feature flag will automatically enable this survey for everyone in the + feature flag. + + } + > + {({ value, onChange }) => ( +
+ + {value && ( + } + size="small" + status="stealth" + onClick={() => onChange(null)} + aria-label="close" + /> + )} +
+ )} +
+ + {({ value, onChange }) => ( + <> + +
+ URL + { + onChange({ ...value, urlMatchType: matchTypeVal }) + }} + data-attr="survey-url-matching-type" + options={Object.keys(SurveyUrlMatchTypeLabels).map((key) => ({ + label: SurveyUrlMatchTypeLabels[key], + value: key, + }))} + /> + onChange({ ...value, url: urlVal })} + placeholder="ex: https://app.posthog.com" + fullWidth + /> +
+
+ + onChange({ ...value, selector: selectorVal })} + placeholder="ex: .className or #id" + /> + + +
+ { + if (checked) { + onChange({ + ...value, + seenSurveyWaitPeriodInDays: + value?.seenSurveyWaitPeriodInDays || 30, + }) + } else { + const { seenSurveyWaitPeriodInDays, ...rest } = value || {} + onChange(rest) + } + }} + /> + Do not display this survey to users who have already seen a survey in the last + { + if (val !== undefined && val > 0) { + onChange({ ...value, seenSurveyWaitPeriodInDays: val }) + } + }} + className="w-16" + />{' '} + days. +
+
+ + )} +
+ + + {!hasTargetingFlag && ( + { + setSurveyValue('targeting_flag_filters', { groups: [] }) + setSurveyValue('remove_targeting_flag', false) + }} + > + Add user targeting + + )} + {hasTargetingFlag && ( + <> +
+ +
+ { + setSurveyValue('targeting_flag_filters', null) + setSurveyValue('targeting_flag', null) + setSurveyValue('remove_targeting_flag', true) + }} + > + Remove all user properties + + + )} +
+
+
+
+ +
+ {survey.type !== SurveyType.API ? ( + + {({ value, onChange }) => ( + <> + 1 ? { submitButtonText: 'Next' } : null), + }} + /> + { + onChange(appearance) + }} + /> + + )} + + ) : ( + + )} +
+ + ) +} diff --git a/frontend/src/scenes/surveys/Survey.tsx b/frontend/src/scenes/surveys/Survey.tsx index 8363e5a22a00c..518547a906513 100644 --- a/frontend/src/scenes/surveys/Survey.tsx +++ b/frontend/src/scenes/surveys/Survey.tsx @@ -1,45 +1,23 @@ import { SceneExport } from 'scenes/sceneTypes' import { surveyLogic } from './surveyLogic' import { BindLogic, useActions, useValues } from 'kea' -import { Form, Group } from 'kea-forms' +import { Form } from 'kea-forms' import { PageHeader } from 'lib/components/PageHeader' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { - LemonBanner, - LemonButton, - LemonCheckbox, - LemonCollapse, - LemonDivider, - LemonInput, - LemonSelect, - LemonTabs, - LemonTextArea, - Link, -} from '@posthog/lemon-ui' +import { LemonButton, LemonDivider, Link } from '@posthog/lemon-ui' import { router } from 'kea-router' import { urls } from 'scenes/urls' -import { Field, PureField } from 'lib/forms/Field' -import { - SurveyQuestion, - Survey, - SurveyQuestionType, - SurveyType, - LinkSurveyQuestion, - RatingSurveyQuestion, - SurveyUrlMatchType, -} from '~/types' +import { Survey, SurveyUrlMatchType } from '~/types' import { FlagSelector } from 'scenes/early-access-features/EarlyAccessFeature' -import { IconCancel, IconDelete, IconPlus, IconPlusMini } from 'lib/lemon-ui/icons' import { SurveyView } from './SurveyView' -import { SurveyAppearance } from './SurveyAppearance' -import { SurveyAPIEditor } from './SurveyAPIEditor' -import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' -import { defaultSurveyFieldValues, defaultSurveyAppearance, NewSurvey, SurveyUrlMatchTypeLabels } from './constants' -import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' +import { NewSurvey, SurveyUrlMatchTypeLabels } from './constants' import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' -import { CodeEditor } from 'lib/components/CodeEditors' +import EditSurveyOld from './EditSurveyOld' +import EditSurveyNew from './EditSurveyNew' import { NotFound } from 'lib/components/NotFound' +import { FEATURE_FLAGS } from 'lib/constants' export const scene: SceneExport = { component: SurveyComponent, @@ -71,16 +49,8 @@ export function SurveyComponent({ id }: { id?: string } = {}): JSX.Element { } export function SurveyForm({ id }: { id: string }): JSX.Element { - const { - survey, - surveyLoading, - isEditingSurvey, - hasTargetingFlag, - urlMatchTypeValidationError, - writingHTMLDescription, - } = useValues(surveyLogic) - const { loadSurvey, editingSurvey, setSurveyValue, setDefaultForQuestionType, setWritingHTMLDescription } = - useActions(surveyLogic) + const { survey, surveyLoading, isEditingSurvey, hasTargetingFlag } = useValues(surveyLogic) + const { loadSurvey, editingSurvey } = useActions(surveyLogic) const { featureFlags } = useValues(enabledFeaturesLogic) return ( @@ -116,524 +86,7 @@ export function SurveyForm({ id }: { id: string }): JSX.Element { } /> -
-
- - - - - - - - - - -
Questions
- {survey.questions.map( - (question: LinkSurveyQuestion | SurveyQuestion | RatingSurveyQuestion, index: number) => ( - - - {question.question} - {survey.questions.length > 1 && ( - } - status="primary-alt" - data-attr={`delete-survey-question-${index}`} - onClick={() => { - setSurveyValue( - 'questions', - survey.questions.filter((_, i) => i !== index) - ) - }} - tooltipPlacement="topRight" - /> - )} -
- ), - content: ( -
- - { - const isEditingQuestion = - defaultSurveyFieldValues[question.type].questions[0] - .question !== question.question - const isEditingDescription = - defaultSurveyFieldValues[question.type].questions[0] - .description !== question.description - const isEditingThankYouMessage = - defaultSurveyFieldValues[question.type].appearance - .thankYouMessageHeader !== - survey.appearance.thankYouMessageHeader - setDefaultForQuestionType( - index, - newType, - isEditingQuestion, - isEditingDescription, - isEditingThankYouMessage - ) - }} - options={[ - { label: 'Open text', value: SurveyQuestionType.Open }, - { label: 'Link', value: SurveyQuestionType.Link }, - { label: 'Rating', value: SurveyQuestionType.Rating }, - ...[ - { - label: 'Single choice select', - value: SurveyQuestionType.SingleChoice, - }, - { - label: 'Multiple choice select', - value: SurveyQuestionType.MultipleChoice, - }, - ], - ]} - /> - - {survey.questions.length > 1 && ( - - - - )} - - - - {question.type === SurveyQuestionType.Link && ( - - - - )} - - {({ value, onChange }) => ( - <> - - setWritingHTMLDescription(key === 'html') - } - tabs={[ - { - key: 'text', - label: ( - Text - ), - content: ( - onChange(v)} - /> - ), - }, - { - key: 'html', - label: ( - HTML - ), - content: ( -
- - onChange(v ?? '') - } - height={150} - options={{ - minimap: { enabled: false }, - wordWrap: 'on', - scrollBeyondLastLine: false, - automaticLayout: true, - fixedOverflowWidgets: true, - lineNumbers: 'off', - glyphMargin: false, - folding: false, - }} - /> -
- ), - }, - ]} - /> - {question.description && - question.description - ?.toLowerCase() - .includes(' - Scripts won't run in the survey popup and - we'll remove these on save. Use the API - question mode to run your own scripts in - surveys. - - )} - - )} -
- {question.type === SurveyQuestionType.Rating && ( -
-
- - - - - - -
-
- - - - - - -
-
- )} - {(question.type === SurveyQuestionType.SingleChoice || - question.type === SurveyQuestionType.MultipleChoice) && ( -
- - {({ value, onChange }) => ( -
- {(value || []).map( - (choice: string, index: number) => ( -
- { - const newChoices = [ - ...value, - ] - newChoices[index] = val - onChange(newChoices) - }} - /> - } - size="small" - status="muted" - noPadding - onClick={() => { - const newChoices = [ - ...value, - ] - newChoices.splice(index, 1) - onChange(newChoices) - }} - /> -
- ) - )} -
- {(value || []).length < 6 && ( - } - type="secondary" - fullWidth={false} - onClick={() => { - if (!value) { - onChange(['']) - } else { - onChange([...value, '']) - } - }} - > - Add choice - - )} -
-
- )} -
-
- )} -
- ), - }, - ]} - /> - - ) - )} - {featureFlags[FEATURE_FLAGS.SURVEYS_MULTIPLE_QUESTIONS] && ( - // TODO: Add pay gate mini here once billing is resolved for it - } - onClick={() => { - setSurveyValue('questions', [ - ...survey.questions, - { ...defaultSurveyFieldValues.open.questions[0] }, - ]) - }} - > - Add question - - )} - - - {({ value, onChange }) => ( - <> - onChange({ ...value, displayThankYouMessage: checked })} - /> - {value.displayThankYouMessage && ( - <> - - onChange({ ...value, thankYouMessageHeader: val })} - placeholder="ex: Thank you for your feedback!" - /> - - - - onChange({ ...value, thankYouMessageDescription: val }) - } - minRows={2} - placeholder="ex: We really appreciate it." - /> - - - )} - - )} - - - - - If targeting options are set, the survey will be released to users who match all of - the conditions. If no targeting options are set, the survey{' '} - will be released to everyone. - - - Connecting to a feature flag will automatically enable this survey for everyone in - the feature flag. - - } - > - {({ value, onChange }) => ( -
- - {value && ( - } - size="small" - status="stealth" - onClick={() => onChange(undefined)} - aria-label="close" - /> - )} -
- )} -
- - {({ value, onChange }) => ( - <> - -
- URL - { - onChange({ ...value, urlMatchType: matchTypeVal }) - }} - data-attr="survey-url-matching-type" - options={Object.keys(SurveyUrlMatchTypeLabels).map((key) => ({ - label: SurveyUrlMatchTypeLabels[key], - value: key, - }))} - /> - onChange({ ...value, url: urlVal })} - placeholder="ex: https://app.posthog.com" - fullWidth - /> -
-
- - onChange({ ...value, selector: selectorVal })} - placeholder="ex: .className or #id" - /> - - -
- { - if (checked) { - onChange({ - ...value, - seenSurveyWaitPeriodInDays: - value?.seenSurveyWaitPeriodInDays || 30, - }) - } else { - const { seenSurveyWaitPeriodInDays, ...rest } = value || {} - onChange(rest) - } - }} - /> - Do not display this survey to users who have already seen a survey in the - last - { - if (val !== undefined && val > 0) { - onChange({ ...value, seenSurveyWaitPeriodInDays: val }) - } - }} - className="w-16" - />{' '} - days. -
-
- - )} -
- - - {!hasTargetingFlag && ( - { - setSurveyValue('targeting_flag_filters', { groups: [] }) - setSurveyValue('remove_targeting_flag', false) - }} - > - Add user targeting - - )} - {hasTargetingFlag && ( - <> -
- -
- { - setSurveyValue('targeting_flag_filters', null) - setSurveyValue('targeting_flag', null) - setSurveyValue('remove_targeting_flag', true) - }} - > - Remove all user properties - - - )} -
-
-
-
- -
- {survey.type !== SurveyType.API ? ( - - {({ value, onChange }) => ( - { - onChange(appearance) - }} - link={ - survey.questions[0].type === SurveyQuestionType.Link - ? survey.questions[0].link - : undefined - } - appearance={value || defaultSurveyAppearance} - /> - )} - - ) : ( - - )} -
- + {featureFlags[FEATURE_FLAGS.SURVEYS_NEW_CREATION_FLOW] ? : } diff --git a/frontend/src/scenes/surveys/SurveyAPIEditor.tsx b/frontend/src/scenes/surveys/SurveyAPIEditor.tsx index 02fc80e8708ba..c474d973db1a9 100644 --- a/frontend/src/scenes/surveys/SurveyAPIEditor.tsx +++ b/frontend/src/scenes/surveys/SurveyAPIEditor.tsx @@ -8,7 +8,7 @@ export function SurveyAPIEditor({ survey }: { survey: Survey | NewSurvey }): JSX id: survey.id, name: survey.name, description: survey.description, - type: survey.type, + type: 'api', linked_flag_key: survey.linked_flag ? survey.linked_flag.key : null, targeting_flag_key: survey.targeting_flag ? survey.targeting_flag.key : null, questions: survey.questions, @@ -18,11 +18,8 @@ export function SurveyAPIEditor({ survey }: { survey: Survey | NewSurvey }): JSX } return ( -
-

API survey response

- - {JSON.stringify(apiSurvey, null, 2)} - -
+ + {JSON.stringify(apiSurvey, null, 2)} + ) } diff --git a/frontend/src/scenes/surveys/SurveyAppearance.tsx b/frontend/src/scenes/surveys/SurveyAppearance.tsx index 504536949f387..da823a2cb01d2 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyAppearance.tsx @@ -21,7 +21,7 @@ import { } from './SurveyAppearanceUtils' import { surveysLogic } from './surveysLogic' import { useValues } from 'kea' -import { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { sanitize } from 'dompurify' @@ -33,23 +33,31 @@ interface SurveyAppearanceProps { surveyQuestionItem: RatingSurveyQuestion | SurveyQuestion | MultipleSurveyQuestion description?: string | null link?: string | null - readOnly?: boolean + preview?: boolean +} + +interface CustomizationProps { + appearance: SurveyAppearanceType + surveyQuestionItem: RatingSurveyQuestion | SurveyQuestion | MultipleSurveyQuestion onAppearanceChange: (appearance: SurveyAppearanceType) => void } +interface ButtonProps { + link?: string | null + type?: SurveyQuestionType + onSubmit: () => void + appearance: SurveyAppearanceType + children: React.ReactNode +} + const Button = ({ link, type, onSubmit, appearance, children, -}: { - link?: string | null - type?: SurveyQuestionType - onSubmit: () => void - appearance: SurveyAppearanceType - children: React.ReactNode -}): JSX.Element => { + ...other +}: ButtonProps & React.HTMLProps): JSX.Element => { const [textColor, setTextColor] = useState('black') const ref = useRef(null) @@ -70,6 +78,7 @@ const Button = ({ onSubmit() }} style={{ color: textColor, backgroundColor: appearance.submitButtonColor }} + {...other} > {children || 'Submit'} @@ -83,155 +92,140 @@ export function SurveyAppearance({ surveyQuestionItem, description, link, - readOnly, - onAppearanceChange, + preview, }: SurveyAppearanceProps): JSX.Element { + return ( +
+ {type === SurveyQuestionType.Rating && ( + undefined} + /> + )} + {(surveyQuestionItem.type === SurveyQuestionType.SingleChoice || + surveyQuestionItem.type === SurveyQuestionType.MultipleChoice) && ( + undefined} + /> + )} + {(surveyQuestionItem.type === SurveyQuestionType.Open || + surveyQuestionItem.type === SurveyQuestionType.Link) && ( + undefined} + /> + )} +
+ ) +} + +export function Customization({ appearance, surveyQuestionItem, onAppearanceChange }: CustomizationProps): JSX.Element { const { whitelabelAvailable } = useValues(surveysLogic) const { featureFlags } = useValues(featureFlagLogic) - const [showThankYou, setShowThankYou] = useState(false) - const [hideSubmittedSurvey, setHideSubmittedSurvey] = useState(false) - - useEffect(() => { - if (appearance.displayThankYouMessage && showThankYou) { - setHideSubmittedSurvey(true) - } else { - setHideSubmittedSurvey(false) - } - }, [showThankYou]) - return ( -
-

Preview

- {!hideSubmittedSurvey && ( +
+
Background color
+ onAppearanceChange({ ...appearance, backgroundColor })} + /> +
Border color
+ onAppearanceChange({ ...appearance, borderColor })} + /> + {featureFlags[FEATURE_FLAGS.SURVEYS_POSITIONS] && ( <> - {type === SurveyQuestionType.Rating && ( - appearance.displayThankYouMessage && setShowThankYou(true)} - /> - )} - {(surveyQuestionItem.type === SurveyQuestionType.SingleChoice || - surveyQuestionItem.type === SurveyQuestionType.MultipleChoice) && ( - appearance.displayThankYouMessage && setShowThankYou(true)} - /> - )} - {(surveyQuestionItem.type === SurveyQuestionType.Open || - surveyQuestionItem.type === SurveyQuestionType.Link) && ( - appearance.displayThankYouMessage && setShowThankYou(true)} - /> - )} +
Position
+
+ {['left', 'center', 'right'].map((position) => { + return ( + onAppearanceChange({ ...appearance, position })} + active={appearance.position === position} + > + {position} + + ) + })} +
)} - {showThankYou && } - {!readOnly && ( -
-
Background color
+ {surveyQuestionItem.type === SurveyQuestionType.Rating && ( + <> +
Rating button color
onAppearanceChange({ ...appearance, backgroundColor })} + value={appearance?.ratingButtonColor} + onChange={(ratingButtonColor) => onAppearanceChange({ ...appearance, ratingButtonColor })} /> -
Border color
+
Rating button active color
onAppearanceChange({ ...appearance, borderColor })} + value={appearance?.ratingButtonActiveColor} + onChange={(ratingButtonActiveColor) => + onAppearanceChange({ ...appearance, ratingButtonActiveColor }) + } /> - {featureFlags[FEATURE_FLAGS.SURVEYS_POSITIONS] && ( - <> -
Position
-
- {['left', 'center', 'right'].map((position) => { - return ( - onAppearanceChange({ ...appearance, position })} - active={appearance.position === position} - > - {position} - - ) - })} -
- - )} - {surveyQuestionItem.type === SurveyQuestionType.Rating && ( - <> -
Rating button color
- - onAppearanceChange({ ...appearance, ratingButtonColor }) - } - /> -
Rating button active color
- - onAppearanceChange({ ...appearance, ratingButtonActiveColor }) - } - /> - - )} -
Button color
- onAppearanceChange({ ...appearance, submitButtonColor })} - /> -
Button text
+ + )} +
Button color
+ onAppearanceChange({ ...appearance, submitButtonColor })} + /> +
Button text
+ onAppearanceChange({ ...appearance, submitButtonText })} + /> + {surveyQuestionItem.type === SurveyQuestionType.Open && ( + <> +
Placeholder
onAppearanceChange({ ...appearance, submitButtonText })} + value={appearance?.placeholder || defaultSurveyAppearance.placeholder} + onChange={(placeholder) => onAppearanceChange({ ...appearance, placeholder })} /> - {surveyQuestionItem.type === SurveyQuestionType.Open && ( - <> -
Placeholder
- onAppearanceChange({ ...appearance, placeholder })} - /> - - )} -
- - Hide PostHog branding -
- } - onChange={(checked) => onAppearanceChange({ ...appearance, whiteLabel: checked })} - disabledReason={ - !whitelabelAvailable ? 'Upgrade to any paid plan to hide PostHog branding' : null - } - /> -
-
+ )} +
+ + Hide PostHog branding +
+ } + onChange={(checked) => onAppearanceChange({ ...appearance, whiteLabel: checked })} + disabledReason={!whitelabelAvailable ? 'Upgrade to any paid plan to hide PostHog branding' : null} + /> +
) } // This should be synced to the UI of the surveys app plugin -function BaseAppearance({ +export function BaseAppearance({ type, question, appearance, onSubmit, description, link, + preview, }: { type: SurveyQuestionType question: string @@ -239,6 +233,7 @@ function BaseAppearance({ onSubmit: () => void description?: string | null link?: string | null + preview?: boolean }): JSX.Element { const [textColor, setTextColor] = useState('black') const ref = useRef(null) @@ -261,14 +256,18 @@ function BaseAppearance({ }} >
-
- -
+ {!preview && ( +
+ +
+ )}
{question}
{/* Using dangerouslySetInnerHTML is safe here, because it's taking the user's input and showing it to the same user. @@ -279,6 +278,7 @@ function BaseAppearance({ )} {type === SurveyQuestionType.Open && (