Merge pull request #25164 from overleaf/dp-editor-survey

Create Editor Survey

GitOrigin-RevId: dc11ef16c0a00aa846ac7a664dd88e9531e832f2
This commit is contained in:
David
2025-05-02 08:05:47 +00:00
committed by Copybot
parent 32a8142f9c
commit 79f9957b68
8 changed files with 314 additions and 1 deletions
@@ -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 =>
@@ -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) {
@@ -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": "",
@@ -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 (
<OLToastContainer className="editor-survey-toast">
<EditorSurveyContent />
</OLToastContainer>
)
}
const TUTORIAL_KEY = 'editor-popup-ux-survey'
const EditorSurveyContent = () => {
const [easeOfUse, setEaseOfUse] = useState<number | null>(null)
const [meetsMyNeeds, setMeetsMyNeeds] = useState<number | null>(null)
const [page, setPage] = useState<EditorSurveyPage>('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 (
<OLToast
className="editor-survey-question-toast"
content={
<EditorSurveyQuestion
questionText={t('overleaf_is_easy_to_use')}
name="ease-of-use"
buttonText={t('next')}
onButtonClick={() => setPage('meets-my-needs')}
value={easeOfUse}
onValueChange={setEaseOfUse}
onDismiss={dismissSurvey}
/>
}
type="info"
/>
)
}
if (page === 'meets-my-needs') {
return (
<OLToast
className="editor-survey-question-toast"
content={
<EditorSurveyQuestion
questionText={t('overleafs_functionality_meets_my_needs')}
name="meets-my-needs"
buttonText={t('submit_title')}
onButtonClick={onSubmit}
value={meetsMyNeeds}
onValueChange={setMeetsMyNeeds}
onDismiss={dismissSurvey}
/>
}
type="info"
/>
)
}
return <OLToast type="info" content={t('thank_you')} autoHide delay={3000} />
}
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<HTMLInputElement>) => {
const newValue = event.target.value
if (newValue) {
onValueChange(Number(newValue))
}
},
[onValueChange]
)
return (
<div className="editor-survey-question">
<div className="editor-survey-question-top-line">
<div>{t('your_feedback_matters_answer_two_quick_questions')}</div>
<OLIconButton
variant="ghost"
size="sm"
icon="close"
accessibilityLabel={t('close')}
onClick={onDismiss}
/>
</div>
<div>
<label className="editor-survey-question-label" htmlFor={name}>
{questionText}
</label>
<OLForm className="editor-survey-question-form">
<OLFormGroup className="editor-survey-question-options">
{options.map(({ value: optionValue, description }) => (
<OLFormCheckbox
key={optionValue}
name={name}
type="radio"
value={optionValue}
id={optionValue.toString()}
label={optionValue}
checked={optionValue === value}
onChange={onChange}
aria-label={`${optionValue} ${description}`}
/>
))}
</OLFormGroup>
</OLForm>
<div className="editor-survey-option-labels">
<div>{t('strongly_disagree')}</div>
<div>{t('strongly_agree')}</div>
</div>
</div>
<OLButton
size="sm"
variant="secondary"
onClick={onButtonClick}
disabled={!value}
>
{buttonText}
</OLButton>
</div>
)
}
@@ -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 (
<GlobalAlertsProvider>
@@ -44,6 +52,7 @@ export default function IdePage() {
</>
)}
<GlobalToasts />
{showEditorSurvey && <EditorSurvey />}
</GlobalAlertsProvider>
)
}
@@ -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';
@@ -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;
}
+8
View File
@@ -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__</0>. This could be caused by network blocking or a strict browser plugin rule. Please follow our <1>troubleshooting guide</1>.",
"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.",