diff --git a/frontend/src/scenes/surveys/EditSurvey.scss b/frontend/src/scenes/surveys/EditSurvey.scss index 438ff1fc3d3e0..e1334158f0131 100644 --- a/frontend/src/scenes/surveys/EditSurvey.scss +++ b/frontend/src/scenes/surveys/EditSurvey.scss @@ -16,3 +16,10 @@ font-size: 10px; background-color: var(--bg-3000); } + +.SurveyQuestionDragHandle { + font-size: 1.2em; + color: var(--default); + cursor: move; + transform: rotate(90deg); +} diff --git a/frontend/src/scenes/surveys/SurveyEdit.tsx b/frontend/src/scenes/surveys/SurveyEdit.tsx index 456350e651a54..0c939dd20b9b0 100644 --- a/frontend/src/scenes/surveys/SurveyEdit.tsx +++ b/frontend/src/scenes/surveys/SurveyEdit.tsx @@ -1,5 +1,7 @@ import './EditSurvey.scss' +import { DndContext } from '@dnd-kit/core' +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' import { LemonBanner, LemonButton, @@ -50,6 +52,7 @@ import { SurveyMultipleChoiceAppearance, SurveyRatingAppearance, } from './SurveyAppearance' +import { SurveyEditQuestionHeader } from './SurveyEditQuestionRow' import { SurveyFormAppearance } from './SurveyFormAppearance' import { SurveyEditSection, surveyLogic } from './surveyLogic' import { surveysLogic } from './surveysLogic' @@ -112,6 +115,20 @@ export default function SurveyEdit(): JSX.Element { } = useActions(surveyLogic) const { surveysMultipleQuestionsAvailable } = useValues(surveysLogic) const { featureFlags } = useValues(enabledFeaturesLogic) + const sortedItemIds = survey.questions.map((_, idx) => idx.toString()) + + function onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }): void { + function move(arr: SurveyQuestion[], from: number, to: number): SurveyQuestion[] { + const clone = [...arr] + // Remove the element from the array + const [element] = clone.splice(from, 1) + // Insert the element at the new position + clone.splice(to, 0, element) + return clone.map((child) => ({ ...child })) + } + setSurveyValue('questions', move(survey.questions, oldIndex, newIndex)) + setSelectedQuestion(newIndex) + } return (
@@ -133,464 +150,441 @@ export default function SurveyEdit(): JSX.Element { header: 'Steps', content: ( <> - { - setSelectedQuestion(index) + { + if (over && active.id !== over.id) { + onSortEnd({ + oldIndex: sortedItemIds.indexOf(active.id.toString()), + newIndex: sortedItemIds.indexOf(over.id.toString()), + }) + } }} - 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() - setSelectedQuestion(index <= 0 ? 0 : index - 1) - setSurveyValue( - 'questions', - survey.questions.filter( - (_, i) => i !== index - ) - ) - }} - tooltipPlacement="topRight" + > + + { + setSelectedQuestion(index) + }} + panels={[ + ...survey.questions.map( + ( + question: + | LinkSurveyQuestion + | SurveyQuestion + | RatingSurveyQuestion, + index: number + ) => ({ + key: index, + header: ( + - )} -
- ), - 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: SurveyQuestionLabel[ - SurveyQuestionType.Open - ], - value: SurveyQuestionType.Open, - tooltip: () => ( - undefined} - appearance={{ - ...survey.appearance, - whiteLabel: true, - }} - question={{ - type: SurveyQuestionType.Open, - question: - 'Share your thoughts', - description: - 'Optional form description', - }} - /> - ), - }, - { - label: 'Link', - value: SurveyQuestionType.Link, - tooltip: () => ( - undefined} - appearance={{ - ...survey.appearance, - whiteLabel: true, - }} - question={{ - type: SurveyQuestionType.Link, - question: - 'Do you want to join our upcoming webinar?', - buttonText: 'Register', - link: '', - }} - /> - ), - }, - { - label: 'Rating', - value: SurveyQuestionType.Rating, - tooltip: () => ( - undefined} - appearance={{ - ...survey.appearance, - whiteLabel: true, - }} - ratingSurveyQuestion={{ - question: - 'How satisfied are you with our product?', - description: - 'Optional form description.', - display: 'number', - lowerBoundLabel: - 'Not great', - upperBoundLabel: - 'Fantastic', - scale: 5, - type: SurveyQuestionType.Rating, - }} - /> - ), - }, - ...[ - { - label: 'Single choice select', - value: SurveyQuestionType.SingleChoice, - tooltip: () => ( - undefined} - appearance={{ - ...survey.appearance, - whiteLabel: true, - }} - 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, - }} - multipleChoiceQuestion={{ - type: SurveyQuestionType.MultipleChoice, - choices: [ - 'Tutorials', - 'Customer case studies', - 'Product announcements', - ], - question: - 'Which types of content would you like to see more of?', - }} - /> - ), - }, - ], - ]} - /> - - - - - - - {({ value, onChange }) => ( - - )} - - {survey.questions.length > 1 && ( - - - - )} - {question.type === SurveyQuestionType.Link && ( - - - - )} - {question.type === SurveyQuestionType.Rating && ( + ), + 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: SurveyQuestionLabel[ + SurveyQuestionType.Open + ], + value: SurveyQuestionType.Open, + tooltip: () => ( + + undefined + } + appearance={{ + ...survey.appearance, + whiteLabel: true, + }} + question={{ + type: SurveyQuestionType.Open, + question: + 'Share your thoughts', + description: + 'Optional form description', + }} + /> + ), + }, + { + label: 'Link', + value: SurveyQuestionType.Link, + tooltip: () => ( + + undefined + } + appearance={{ + ...survey.appearance, + whiteLabel: true, + }} + question={{ + type: SurveyQuestionType.Link, + question: + 'Do you want to join our upcoming webinar?', + buttonText: + 'Register', + link: '', + }} + /> + ), + }, + { + label: 'Rating', + value: SurveyQuestionType.Rating, + tooltip: () => ( + + undefined + } + appearance={{ + ...survey.appearance, + whiteLabel: true, + }} + ratingSurveyQuestion={{ + question: + 'How satisfied are you with our product?', + description: + 'Optional form description.', + display: 'number', + lowerBoundLabel: + 'Not great', + upperBoundLabel: + 'Fantastic', + scale: 5, + type: SurveyQuestionType.Rating, + }} + /> + ), + }, + ...[ { - label: 'Emoji', - value: 'emoji', + label: 'Single choice select', + value: SurveyQuestionType.SingleChoice, + tooltip: () => ( + + undefined + } + appearance={{ + ...survey.appearance, + whiteLabel: + true, + }} + multipleChoiceQuestion={{ + type: SurveyQuestionType.SingleChoice, + choices: [ + 'Yes', + 'No', + ], + question: + 'Have you found this tutorial useful?', + }} + /> + ), }, - ]} - /> - - - ( + + undefined + } + appearance={{ + ...survey.appearance, + whiteLabel: + true, + }} + multipleChoiceQuestion={{ + type: SurveyQuestionType.MultipleChoice, + choices: [ + 'Tutorials', + 'Customer case studies', + 'Product announcements', + ], + question: + 'Which types of content would you like to see more of?', + }} + /> + ), }, - ...(question.display === - 'number' - ? [ - { - label: '0 - 10', - value: 10, - }, - ] - : []), - ]} - /> - -
-
- - + + + + + + + {({ value, onChange }) => ( + + )} + + {survey.questions.length > 1 && ( + + + )} + {question.type === SurveyQuestionType.Link && ( -
-
- )} - {(question.type === SurveyQuestionType.SingleChoice || - question.type === - SurveyQuestionType.MultipleChoice) && ( -
- - {({ - value: hasOpenChoice, - onChange: toggleHasOpenChoice, - }) => ( - - {({ 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, + )} + {question.type === + SurveyQuestionType.Rating && ( +
+
+ + + + + + +
+
+ + + + + + +
+
+ )} + {(question.type === + SurveyQuestionType.SingleChoice || + question.type === + SurveyQuestionType.MultipleChoice) && ( +
+ + {({ + value: hasOpenChoice, + onChange: toggleHasOpenChoice, + }) => ( + + {({ value, onChange }) => ( +
+ {(value || []).map( + ( + choice: string, + index: number + ) => { + const isOpenChoice = + hasOpenChoice && + index === + value?.length - 1 - ) - onChange( - newChoices - ) - if ( - isOpenChoice - ) { - toggleHasOpenChoice( - false - ) + return ( +
-
- ) - } - )} -
- {(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 - + > + { + 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 - ) - } else { - onChange( - [ - ...value, - '', - ] - ) - } - }} - > - Add choice - - {featureFlags[ - FEATURE_FLAGS - .SURVEYS_OPEN_CHOICE - ] && - !hasOpenChoice && ( + ) + onChange( + newChoices + ) + if ( + isOpenChoice + ) { + toggleHasOpenChoice( + false + ) + } + }} + /> +
+ ) + } + )} +
+ {(value || []) + .length < + 6 && ( + <> @@ -605,137 +599,197 @@ export default function SurveyEdit(): JSX.Element { ) { onChange( [ - 'Other', + '', + ] + ) + } else if ( + hasOpenChoice + ) { + const newChoices = + value.slice( + 0, + -1 + ) + newChoices.push( + '' + ) + newChoices.push( + value[ + value.length - + 1 ] ) + onChange( + newChoices + ) } else { onChange( [ ...value, - 'Other', + '', ] ) } - toggleHasOpenChoice( - true - ) }} > Add - open-ended choice - )} - - )} -
-
+ {featureFlags[ + FEATURE_FLAGS + .SURVEYS_OPEN_CHOICE + ] && + !hasOpenChoice && ( + + } + type="secondary" + fullWidth={ + false + } + onClick={() => { + if ( + !value + ) { + onChange( + [ + 'Other', + ] + ) + } else { + onChange( + [ + ...value, + 'Other', + ] + ) + } + toggleHasOpenChoice( + true + ) + }} + > + Add + open-ended + choice + + )} + + )} +
+
+ )} + )} - )} +
+ )} + +
- )} - - 1 && - index !== survey.questions.length - 1 - ? 'Next' - : survey.appearance.submitButtonText - : question.buttonText - } - /> - -
-
+ + ), + }) ), - }) - ), - ...(survey.appearance.displayThankYouMessage - ? [ - { - key: survey.questions.length, - header: ( -
- Confirmation message - } - status="primary-alt" - data-attr={`delete-survey-confirmation`} - onClick={(e) => { - e.stopPropagation() - setSelectedQuestion( - 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, - }) - } - writingHTMLDescription={ - writingHTMLDescription - } - setWritingHTMLDescription={ - setWritingHTMLDescription - } - textPlaceholder="ex: We really appreciate it." - /> - - - - setSurveyValue('appearance', { - ...survey.appearance, - autoDisappear: checked, - }) - } - /> - - - ), - }, - ] - : []), - ]} - /> + ...(survey.appearance.displayThankYouMessage + ? [ + { + key: survey.questions.length, + header: ( +
+ Confirmation message + } + status="primary-alt" + data-attr={`delete-survey-confirmation`} + onClick={(e) => { + e.stopPropagation() + setSelectedQuestion( + 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, + }) + } + writingHTMLDescription={ + writingHTMLDescription + } + setWritingHTMLDescription={ + setWritingHTMLDescription + } + textPlaceholder="ex: We really appreciate it." + /> + + + + setSurveyValue('appearance', { + ...survey.appearance, + autoDisappear: checked, + }) + } + /> + + + ), + }, + ] + : []), + ]} + /> + +
{featureFlags[FEATURE_FLAGS.SURVEYS_MULTIPLE_QUESTIONS] && (
diff --git a/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx new file mode 100644 index 0000000000000..ea02574f293b8 --- /dev/null +++ b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx @@ -0,0 +1,77 @@ +import './EditSurvey.scss' + +import { DraggableSyntheticListeners } from '@dnd-kit/core' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { LemonButton } from '@posthog/lemon-ui' +import { IconDelete, SortableDragIcon } from 'lib/lemon-ui/icons' + +import { Survey } from '~/types' + +import { NewSurvey } from './constants' + +type SurveyQuestionHeaderProps = { + index: number + survey: Survey | NewSurvey + setSelectedQuestion: (index: number) => void + setSurveyValue: (key: string, value: any) => void +} + +const DragHandle = ({ listeners }: { listeners: DraggableSyntheticListeners | undefined }): JSX.Element => ( + + + +) + +export function SurveyEditQuestionHeader({ + index, + survey, + setSelectedQuestion, + setSurveyValue, +}: SurveyQuestionHeaderProps): JSX.Element { + const { setNodeRef, attributes, transform, transition, listeners, isDragging } = useSortable({ + id: index.toString(), + }) + + const questionsStartElements = [ + survey.questions.length > 1 ? : null, + ].filter(Boolean) + + return ( +
+
+ {questionsStartElements.length ?
{questionsStartElements}
: null} + + + Question {index + 1}. {survey.questions[index].question} + +
+ {survey.questions.length > 1 && ( + } + status="primary-alt" + data-attr={`delete-survey-question-${index}`} + onClick={(e) => { + e.stopPropagation() + setSelectedQuestion(index <= 0 ? 0 : index - 1) + setSurveyValue( + 'questions', + survey.questions.filter((_, i) => i !== index) + ) + }} + tooltipPlacement="topRight" + /> + )} +
+ ) +}