Explorar o código

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

Create Editor Survey

GitOrigin-RevId: dc11ef16c0a00aa846ac7a664dd88e9531e832f2
David hai 1 ano
pai
achega
79f9957b68

+ 1 - 0
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 =>

+ 1 - 0
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) {

+ 9 - 0
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": "",

+ 211 - 0
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 (
+    <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 - 1
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 (
     <GlobalAlertsProvider>
@@ -44,6 +52,7 @@ export default function IdePage() {
         </>
       )}
       <GlobalToasts />
+      {showEditorSurvey && <EditorSurvey />}
     </GlobalAlertsProvider>
   )
 }

+ 1 - 0
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';

+ 73 - 0
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;
+}

+ 8 - 0
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__</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.",