diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 377ed172b7..e42c6e511f 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -356,6 +356,7 @@ const _ProjectController = { 'overleaf-assist-bundle', 'wf-feature-rebrand', 'word-count-client', + 'editor-popup-ux-survey', ].filter(Boolean) const getUserValues = async userId => diff --git a/services/web/app/src/Features/Tutorial/TutorialController.mjs b/services/web/app/src/Features/Tutorial/TutorialController.mjs index 9afb5bb245..a8d5ab3145 100644 --- a/services/web/app/src/Features/Tutorial/TutorialController.mjs +++ b/services/web/app/src/Features/Tutorial/TutorialController.mjs @@ -11,6 +11,7 @@ const VALID_KEYS = [ 'history-restore-promo', 'us-gov-banner', 'us-gov-banner-fedramp', + 'editor-popup-ux-survey', ] async function completeTutorial(req, res, next) { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 667546134e..b82b758f85 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -102,6 +102,7 @@ "after_that_well_bill_you_x_total_y_subtotal_z_tax_annually_on_date_unless_you_cancel": "", "aggregate_changed": "", "aggregate_to": "", + "agree": "", "agree_with_the_terms": "", "ai_assist_in_overleaf_is_included_via_writefull": "", "ai_assistance_to_help_you": "", @@ -401,6 +402,7 @@ "disable_stop_on_first_error": "", "disabled": "", "disabling": "", + "disagree": "", "disconnected": "", "discount": "", "discount_of": "", @@ -1047,6 +1049,7 @@ "need_more_than_x_licenses": "", "need_to_add_new_primary_before_remove": "", "need_to_leave": "", + "neither_agree_nor_disagree": "", "new_compile_domain_notice": "", "new_file": "", "new_folder": "", @@ -1143,8 +1146,10 @@ "overall_theme": "", "overleaf": "", "overleaf_history_system": "", + "overleaf_is_easy_to_use": "", "overleaf_labs": "", "overleaf_logo": "", + "overleafs_functionality_meets_my_needs": "", "overview": "", "overwrite": "", "overwriting_the_original_folder": "", @@ -1636,6 +1641,8 @@ "stop_on_validation_error": "", "store_your_work": "", "stretch_width_to_text": "", + "strongly_agree": "", + "strongly_disagree": "", "student": "", "student_disclaimer": "", "subject": "", @@ -1704,6 +1711,7 @@ "test_configuration_successful": "", "tex_live_version": "", "texgpt": "", + "thank_you": "", "thank_you_exclamation": "", "thank_you_for_your_feedback": "", "thanks_for_being_part_of_this_labs_experiment_your_feedback_will_help_us_make_the_new_editor_the_best_yet": "", @@ -2123,6 +2131,7 @@ "your_current_plan_gives_you": "", "your_current_plan_supports_up_to_x_licenses": "", "your_current_project_will_revert_to_the_version_from_time": "", + "your_feedback_matters_answer_two_quick_questions": "", "your_git_access_info": "", "your_git_access_info_bullet_1": "", "your_git_access_info_bullet_2": "", diff --git a/services/web/frontend/js/features/ide-react/components/editor-survey.tsx b/services/web/frontend/js/features/ide-react/components/editor-survey.tsx new file mode 100644 index 0000000000..e28aa7698d --- /dev/null +++ b/services/web/frontend/js/features/ide-react/components/editor-survey.tsx @@ -0,0 +1,211 @@ +import OLButton from '@/features/ui/components/ol/ol-button' +import OLForm from '@/features/ui/components/ol/ol-form' +import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLIconButton from '@/features/ui/components/ol/ol-icon-button' +import { OLToast } from '@/features/ui/components/ol/ol-toast' +import { OLToastContainer } from '@/features/ui/components/ol/ol-toast-container' +import { useEditorContext } from '@/shared/context/editor-context' +import useTutorial from '@/shared/hooks/promotions/use-tutorial' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { sendMB } from '@/infrastructure/event-tracking' +import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils' +import { useTranslation } from 'react-i18next' + +type EditorSurveyPage = 'ease-of-use' | 'meets-my-needs' | 'thank-you' + +export default function EditorSurvey() { + return ( + + + + ) +} + +const TUTORIAL_KEY = 'editor-popup-ux-survey' + +const EditorSurveyContent = () => { + const [easeOfUse, setEaseOfUse] = useState(null) + const [meetsMyNeeds, setMeetsMyNeeds] = useState(null) + const [page, setPage] = useState('ease-of-use') + const { inactiveTutorials } = useEditorContext() + const hasCompletedSurvey = inactiveTutorials.includes(TUTORIAL_KEY) + const newEditor = useIsNewEditorEnabled() + + const { t } = useTranslation() + + const { + tryShowingPopup: tryShowingSurvey, + showPopup: showSurvey, + dismissTutorial: dismissSurvey, + completeTutorial: completeSurvey, + } = useTutorial(TUTORIAL_KEY, { + name: TUTORIAL_KEY, + }) + + useEffect(() => { + if (!hasCompletedSurvey) { + tryShowingSurvey() + } + }, [hasCompletedSurvey, tryShowingSurvey]) + + const onSubmit = useCallback(() => { + sendMB('editor-survey-submit', { + easeOfUse, + meetsMyNeeds, + newEditor, + }) + setPage('thank-you') + completeSurvey({ event: 'promo-click', action: 'complete' }) + }, [easeOfUse, meetsMyNeeds, completeSurvey, newEditor]) + + if (!showSurvey && page !== 'thank-you') { + return null + } + + if (page === 'ease-of-use') { + return ( + setPage('meets-my-needs')} + value={easeOfUse} + onValueChange={setEaseOfUse} + onDismiss={dismissSurvey} + /> + } + type="info" + /> + ) + } + if (page === 'meets-my-needs') { + return ( + + } + type="info" + /> + ) + } + + return +} + +const EditorSurveyQuestion = ({ + onDismiss, + name, + questionText, + buttonText, + onButtonClick, + value, + onValueChange, +}: { + onDismiss: () => void + name: string + questionText: string + buttonText: string + onButtonClick: () => void + value: number | null + onValueChange: (newValue: number) => void +}) => { + const { t } = useTranslation() + + const options = useMemo( + () => [ + { + value: 1, + description: t('strongly_disagree'), + }, + { + value: 2, + description: t('disagree'), + }, + { + value: 3, + description: t('neither_agree_nor_disagree'), + }, + { + value: 4, + description: t('agree'), + }, + { + value: 5, + description: t('strongly_agree'), + }, + ], + [t] + ) + + const onChange = useCallback( + (event: React.ChangeEvent) => { + const newValue = event.target.value + if (newValue) { + onValueChange(Number(newValue)) + } + }, + [onValueChange] + ) + + return ( +
+
+
{t('your_feedback_matters_answer_two_quick_questions')}
+ +
+
+ + + + {options.map(({ value: optionValue, description }) => ( + + ))} + + +
+
{t('strongly_disagree')}
+
{t('strongly_agree')}
+
+
+ + {buttonText} + +
+ ) +} diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx index fa235560e8..6a03d5b205 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx @@ -10,7 +10,12 @@ import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-e import { Modals } from '@/features/ide-react/components/modals/modals' import { GlobalAlertsProvider } from '@/features/ide-react/context/global-alerts-context' import { GlobalToasts } from '../global-toasts' -import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils' +import { + canUseNewEditor, + useIsNewEditorEnabled, +} from '@/features/ide-redesign/utils/new-editor-utils' +import EditorSurvey from '../editor-survey' +import { useFeatureFlag } from '@/shared/context/split-test-context' const MainLayoutNew = lazy( () => import('@/features/ide-redesign/components/main-layout') @@ -27,6 +32,9 @@ export default function IdePage() { useHasLintingError() // pass editor:lint hasLintingError to the compiler const newEditor = useIsNewEditorEnabled() + const canAccessNewEditor = canUseNewEditor() + const editorSurveyFlag = useFeatureFlag('editor-popup-ux-survey') + const showEditorSurvey = editorSurveyFlag && !canAccessNewEditor return ( @@ -44,6 +52,7 @@ export default function IdePage() { )} + {showEditorSurvey && } ) } diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss index 24a65e6273..a3adc98819 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss @@ -36,6 +36,7 @@ @import 'editor/table-generator-column-width-modal'; @import 'editor/math-preview'; @import 'editor/references-search'; +@import 'editor/editor-survey'; @import 'website-redesign'; @import 'group-settings'; @import 'templates-v2'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/editor-survey.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/editor-survey.scss new file mode 100644 index 0000000000..5041becc5a --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/editor-survey.scss @@ -0,0 +1,73 @@ +.editor-survey-toast { + position: fixed; + bottom: 12px; + right: 12px; + z-index: 1000; +} + +.editor-survey-question-toast { + .notification-icon { + display: none; + } +} + +.editor-survey-question { + display: flex; + flex-direction: column; + gap: 16px; + font-size: var(--font-size-02); + + .btn { + align-self: flex-end; + } + + .form-group { + margin-bottom: 0; + } + + .form-check { + display: flex; + flex-direction: column-reverse; + align-items: center; + padding-left: 0; + gap: 2px; + } + + .form-check-input { + margin: 4px; + } +} + +.editor-survey-question-top-line { + display: flex; + justify-content: space-between; + align-items: center; + + .btn-ghost { + --bs-btn-bg: transparent; + + margin-left: 8px; + } +} + +.editor-survey-question-label { + font-weight: 600; + margin-bottom: 8px; +} + +.editor-survey-question-form { + display: flex; +} + +.editor-survey-question-options { + display: flex; + gap: 32px; + margin: auto; +} + +.editor-survey-option-labels { + display: flex; + justify-content: space-between; + color: var(--content-secondary); + margin-top: 4px; +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 5f0ecd7b4b..b77691d277 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -124,6 +124,7 @@ "after_that_well_bill_you_x_total_y_subtotal_z_tax_annually_on_date_unless_you_cancel": "After that, we’ll bill you __totalAmount__ (__subtotalAmount__ + __taxAmount__ tax) annually on __date__, unless you cancel.", "aggregate_changed": "Changed", "aggregate_to": "to", + "agree": "Agree", "agree_with_the_terms": "I agree with the Overleaf terms", "ai_assist_in_overleaf_is_included_via_writefull": "AI Assist in Overleaf is included as part of your Writefull subscription. You can cancel or manage your access to AI Assist in your Writefull subscription settings.", "ai_assistance_to_help_you": "AI assistance to help you fix LaTeX errors", @@ -524,6 +525,7 @@ "disable_stop_on_first_error": "Disable “Stop on first error”", "disabled": "Disabled", "disabling": "Disabling", + "disagree": "Disagree", "disconnected": "Disconnected", "discount": "Discount", "discount_of": "Discount of __amount__", @@ -1375,6 +1377,7 @@ "need_more_than_x_licenses": "Need more than __x__ licenses?", "need_to_add_new_primary_before_remove": "You’ll need to add a new primary email address before you can remove this one.", "need_to_leave": "Need to leave?", + "neither_agree_nor_disagree": "Neither agree nor disagree", "new_compile_domain_notice": "We’ve recently migrated PDF downloads to a new domain. Something might be blocking your browser from accessing that new domain, <0>__compilesUserContentDomain__. This could be caused by network blocking or a strict browser plugin rule. Please follow our <1>troubleshooting guide.", "new_file": "New file", "new_folder": "New folder", @@ -1514,10 +1517,12 @@ "overleaf_group_plans": "Overleaf group plans", "overleaf_history_system": "Overleaf History System", "overleaf_individual_plans": "Overleaf individual plans", + "overleaf_is_easy_to_use": "Overleaf is easy to use.", "overleaf_labs": "Overleaf Labs", "overleaf_logo": "Overleaf logo", "overleaf_plans_and_pricing": "overleaf plans and pricing", "overleaf_template_gallery": "overleaf template gallery", + "overleafs_functionality_meets_my_needs": "Overleaf’s functionality meets my needs.", "overview": "Overview", "overwrite": "Overwrite", "overwriting_the_original_folder": "Overwriting the original folder will delete it and all the files it contains.", @@ -2128,6 +2133,8 @@ "stop_on_validation_error": "Check syntax before compile", "store_your_work": "Store your work on your own infrastructure", "stretch_width_to_text": "Stretch width to text", + "strongly_agree": "Strongly agree", + "strongly_disagree": "Strongly disagree", "student": "Student", "student_disclaimer": "The educational discount applies to all students at secondary and postsecondary institutions (schools and universities). We may contact you to confirm that you’re eligible for the discount.", "student_verification_required": "Student verification required", @@ -2683,6 +2690,7 @@ "your_current_plan_gives_you": "By pausing your subscription, you’ll be able to access your premium features faster when you need them again.", "your_current_plan_supports_up_to_x_licenses": "Your current plan supports up to __users__ licenses.", "your_current_project_will_revert_to_the_version_from_time": "Your current project will revert to the version from __timestamp__", + "your_feedback_matters_answer_two_quick_questions": "Your feedback matters! Answer two quick questions.", "your_git_access_info": "Your Git authentication tokens should be entered whenever you’re prompted for a password.", "your_git_access_info_bullet_1": "You can have up to 10 tokens.", "your_git_access_info_bullet_2": "If you reach the maximum limit, you’ll need to delete a token before you can generate a new one.",