Merge pull request #29821 from overleaf/dp-editor-redesign-opt-out
Prepare editor redesign for opt-out phase GitOrigin-RevId: 5831970ff27e5c20f22c68b83471b6b832c2b2b4
This commit is contained in:
@@ -458,6 +458,7 @@ const _ProjectController = {
|
||||
'writefull-figure-generator',
|
||||
'writefull-asymetric-queue-size-per-model',
|
||||
'pdf-dark-mode',
|
||||
'editor-redesign-opt-out',
|
||||
].filter(Boolean)
|
||||
|
||||
const getUserValues = async userId =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
||||
const SPLIT_TEST_USER_CUTOFF_DATE = new Date(Date.UTC(2025, 8, 23, 13, 0, 0)) // 2pm British Summer Time on September 23, 2025
|
||||
const NEW_USER_CUTOFF_DATE = new Date(Date.UTC(2025, 10, 12, 12, 0, 0)) // 12pm GMT on November 12, 2025
|
||||
|
||||
async function getEnableNewEditorDefault(req, res, user) {
|
||||
async function getEnableNewEditorLegacyDefault(req, res, user) {
|
||||
if (req.query['existing-user-override'] === 'true') {
|
||||
return false
|
||||
}
|
||||
@@ -31,7 +31,22 @@ async function getEnableNewEditorDefault(req, res, user) {
|
||||
}
|
||||
|
||||
async function buildUserSettings(req, res, user) {
|
||||
const defaultEnableNewEditor = await getEnableNewEditorDefault(req, res, user)
|
||||
const defaultLegacyEnableNewEditor = await getEnableNewEditorLegacyDefault(
|
||||
req,
|
||||
res,
|
||||
user
|
||||
)
|
||||
|
||||
const enableNewEditorStageFour = user.ace.enableNewEditorStageFour ?? true
|
||||
const enableNewEditorLegacy =
|
||||
user.ace.enableNewEditor ?? defaultLegacyEnableNewEditor
|
||||
|
||||
const assignment = await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'editor-redesign-opt-out'
|
||||
)
|
||||
const isOptOutEnabled = assignment.variant === 'enabled'
|
||||
|
||||
return {
|
||||
mode: user.ace.mode,
|
||||
@@ -47,7 +62,10 @@ async function buildUserSettings(req, res, user) {
|
||||
mathPreview: user.ace.mathPreview,
|
||||
breadcrumbs: user.ace.breadcrumbs,
|
||||
referencesSearchMode: user.ace.referencesSearchMode,
|
||||
enableNewEditor: user.ace.enableNewEditor ?? defaultEnableNewEditor,
|
||||
enableNewEditor: isOptOutEnabled
|
||||
? enableNewEditorStageFour
|
||||
: enableNewEditorLegacy,
|
||||
enableNewEditorLegacy,
|
||||
darkModePdf: user.ace.darkModePdf ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ const VALID_KEYS = [
|
||||
'groups-enterprise-banner-repeat',
|
||||
'new-editor-opt-in',
|
||||
'new-editor-intro',
|
||||
'new-editor-intro-2',
|
||||
'old-editor-warning-tooltip',
|
||||
]
|
||||
|
||||
async function completeTutorial(req, res, next) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { expressify } from '@overleaf/promise-utils'
|
||||
import { acceptsJson } from '../../infrastructure/RequestContentTypeDetection.mjs'
|
||||
import Modules from '../../infrastructure/Modules.mjs'
|
||||
import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.mjs'
|
||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
||||
|
||||
async function _sendSecurityAlertClearedSessions(user) {
|
||||
const emailOptions = {
|
||||
@@ -405,7 +406,18 @@ async function updateUserSettings(req, res, next) {
|
||||
user.ace.referencesSearchMode = mode
|
||||
}
|
||||
if (body.enableNewEditor != null) {
|
||||
user.ace.enableNewEditor = Boolean(body.enableNewEditor)
|
||||
const assignment = await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'editor-redesign-opt-out'
|
||||
)
|
||||
const isOptOutStageEnabled = assignment.variant === 'enabled'
|
||||
|
||||
if (isOptOutStageEnabled) {
|
||||
user.ace.enableNewEditorStageFour = Boolean(body.enableNewEditor)
|
||||
} else {
|
||||
user.ace.enableNewEditor = Boolean(body.enableNewEditor)
|
||||
}
|
||||
}
|
||||
if (body.darkModePdf != null) {
|
||||
user.ace.darkModePdf = Boolean(body.darkModePdf)
|
||||
|
||||
@@ -100,7 +100,11 @@ export const UserSchema = new Schema(
|
||||
mathPreview: { type: Boolean, default: true },
|
||||
breadcrumbs: { type: Boolean, default: true },
|
||||
referencesSearchMode: { type: String, default: 'advanced' }, // 'advanced' or 'simple'
|
||||
// enableNewEditor is being phased out in favor of enableNewEditorStageFour
|
||||
// when moving the new editor to opt out (stage 4). However, we need to keep the
|
||||
// old field for determining whether to show promotional material to users.
|
||||
enableNewEditor: { type: Boolean },
|
||||
enableNewEditorStageFour: { type: Boolean },
|
||||
darkModePdf: { type: Boolean, default: false },
|
||||
},
|
||||
features: {
|
||||
|
||||
@@ -1777,6 +1777,7 @@
|
||||
"suggested_fix_for_error_in_path": "",
|
||||
"suggestion_applied": "",
|
||||
"suggests_code_completions_while_typing": "",
|
||||
"support_for_the_old_editor_is_ending_soon": "",
|
||||
"support_for_your_browser_is_ending_soon": "",
|
||||
"supports_up_to_x_licenses": "",
|
||||
"sure_you_want_to_cancel_plan_change": "",
|
||||
@@ -1787,6 +1788,8 @@
|
||||
"switch_compile_mode_for_faster_draft_compilation": "",
|
||||
"switch_easily_between_your_files_comments_track_changes_and_more": "",
|
||||
"switch_to_editor": "",
|
||||
"switch_to_new_editor_design": "",
|
||||
"switch_to_new_look": "",
|
||||
"switch_to_pdf": "",
|
||||
"switch_to_personal_email_to_keep_your_accounts_separate": "",
|
||||
"switch_to_standard_plan": "",
|
||||
@@ -1844,6 +1847,7 @@
|
||||
"the_following_folder_already_exists_in_this_project": "",
|
||||
"the_following_folder_already_exists_in_this_project_plural": "",
|
||||
"the_latex_engine_used_for_compiling": "",
|
||||
"the_new_and_improved_overleaf_editor_design": "",
|
||||
"the_new_overleaf_editor_info": "",
|
||||
"the_next_payment_will_be_collected_on": "",
|
||||
"the_original_text_has_changed": "",
|
||||
@@ -2156,6 +2160,7 @@
|
||||
"we_do_not_share_personal_information": "",
|
||||
"we_got_your_request": "",
|
||||
"we_logged_you_in": "",
|
||||
"we_recommend_switching_to_the_new_editor_design_now_so_you_have_time_to_get_to_know_it": "",
|
||||
"we_sent_code": "",
|
||||
"we_sent_new_code": "",
|
||||
"we_will_charge_you_now_for_the_cost_of_your_additional_licenses_based_on_remaining_months": "",
|
||||
|
||||
+15
-2
@@ -1,14 +1,23 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import OLButton from '../../shared/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSwitchEnableNewEditorState } from '../ide-redesign/hooks/use-switch-enable-new-editor-state'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import OldEditorWarningTooltip from '../ide-redesign/components/old-editor-warning-tooltip'
|
||||
|
||||
const TryNewEditorButton = () => {
|
||||
const { t } = useTranslation()
|
||||
const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState()
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out')
|
||||
const [buttonElt, setButtonElt] = useState<HTMLButtonElement | null>(null)
|
||||
const buttonRef = useCallback((node: HTMLButtonElement) => {
|
||||
if (node !== null) {
|
||||
setButtonElt(node)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
sendEvent('switch-to-new-editor', {
|
||||
@@ -25,10 +34,14 @@ const TryNewEditorButton = () => {
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isLoading={loading}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<MaterialIcon type="fiber_new" />
|
||||
{t('try_the_new_editor_design')}
|
||||
{isNewEditorOptOutStage
|
||||
? t('switch_to_new_look')
|
||||
: t('try_the_new_editor_design')}
|
||||
</OLButton>
|
||||
{isNewEditorOptOutStage && <OldEditorWarningTooltip target={buttonElt} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,25 @@ import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsave
|
||||
import SystemMessages from '@/shared/components/system-messages'
|
||||
import NewEditorPromoModal from '@/features/ide-redesign/components/new-editor-promo-modal'
|
||||
import NewEditorIntroModal from '@/features/ide-redesign/components/new-editor-intro-modal'
|
||||
import NewEditorOptOutIntroModal from '@/features/ide-redesign/components/new-editor-opt-out-intro-modal'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export const Modals = memo(() => {
|
||||
const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out')
|
||||
|
||||
return (
|
||||
<>
|
||||
<ForceDisconnected />
|
||||
<UnsavedDocs />
|
||||
<SystemMessages />
|
||||
<NewEditorPromoModal />
|
||||
<NewEditorIntroModal />
|
||||
{isNewEditorOptOutStage ? (
|
||||
<NewEditorOptOutIntroModal />
|
||||
) : (
|
||||
<>
|
||||
<NewEditorPromoModal />
|
||||
<NewEditorIntroModal />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useIsNewToNewEditor } from '../utils/new-editor-utils'
|
||||
import { useNewEditorTourContext } from '../contexts/new-editor-tour-context'
|
||||
import promoVideo from './new-editor-promo-video.mp4'
|
||||
|
||||
const TUTORIAL_KEY = 'new-editor-intro-2'
|
||||
|
||||
export default function NewEditorOptOutIntroModal() {
|
||||
const { inactiveTutorials } = useEditorContext()
|
||||
const {
|
||||
tryShowingPopup,
|
||||
showPopup: showModal,
|
||||
dismissTutorial,
|
||||
completeTutorial,
|
||||
clearPopup,
|
||||
} = useTutorial(TUTORIAL_KEY, {
|
||||
name: TUTORIAL_KEY,
|
||||
})
|
||||
const { startTour } = useNewEditorTourContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [hasShown, setHasShown] = useState(false)
|
||||
const isNewToNewEditor = useIsNewToNewEditor()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isNewToNewEditor &&
|
||||
!hasShown &&
|
||||
!inactiveTutorials.includes(TUTORIAL_KEY)
|
||||
) {
|
||||
tryShowingPopup('notification-prompt')
|
||||
setHasShown(true)
|
||||
}
|
||||
}, [tryShowingPopup, inactiveTutorials, isNewToNewEditor, hasShown])
|
||||
|
||||
const startProductTour = useCallback(() => {
|
||||
completeTutorial({ event: 'notification-click', action: 'complete' })
|
||||
startTour()
|
||||
clearPopup()
|
||||
}, [completeTutorial, startTour, clearPopup])
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
dismissTutorial('notification-dismiss')
|
||||
clearPopup()
|
||||
}, [dismissTutorial, clearPopup])
|
||||
|
||||
return (
|
||||
<OLModal show={showModal} onHide={closeModal}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('introducing_overleafs_new_look')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody className="new-editor-intro-modal-body">
|
||||
<div>{t('the_new_and_improved_overleaf_editor_design')}</div>
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video autoPlay loop muted>
|
||||
<source src={promoVideo} type="video/mp4" />
|
||||
</video>
|
||||
<div>
|
||||
{t('weve_made_it_easier_to_find_and_use_the_tools_you_need_today')}
|
||||
</div>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton onClick={startProductTour} variant="primary">
|
||||
{t('explore_what_s_new')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { Overlay, Popover } from 'react-bootstrap'
|
||||
import Close from '@/shared/components/close'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useSwitchEnableNewEditorState } from '../hooks/use-switch-enable-new-editor-state'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { canUseNewEditor } from '../utils/new-editor-utils'
|
||||
|
||||
const TUTORIAL_KEY = 'old-editor-warning-tooltip'
|
||||
|
||||
export default function OldEditorWarningTooltip({
|
||||
target,
|
||||
}: {
|
||||
target: HTMLElement | null
|
||||
}) {
|
||||
const { inactiveTutorials } = useEditorContext()
|
||||
const { t } = useTranslation()
|
||||
const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState()
|
||||
|
||||
const {
|
||||
tryShowingPopup,
|
||||
showPopup,
|
||||
dismissTutorial,
|
||||
completeTutorial,
|
||||
clearPopup,
|
||||
} = useTutorial(TUTORIAL_KEY, {
|
||||
name: TUTORIAL_KEY,
|
||||
})
|
||||
const [hasShown, setHasShown] = useState(false)
|
||||
const canShow = canUseNewEditor()
|
||||
|
||||
useEffect(() => {
|
||||
if (canShow && !hasShown && !inactiveTutorials.includes(TUTORIAL_KEY)) {
|
||||
tryShowingPopup('notification-prompt')
|
||||
setHasShown(true)
|
||||
}
|
||||
}, [tryShowingPopup, inactiveTutorials, hasShown, canShow])
|
||||
|
||||
const onSwitch = useCallback(() => {
|
||||
completeTutorial({ event: 'notification-click', action: 'complete' })
|
||||
setEditorRedesignStatus(true)
|
||||
}, [setEditorRedesignStatus, completeTutorial])
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
dismissTutorial('notification-dismiss')
|
||||
clearPopup()
|
||||
}, [dismissTutorial, clearPopup])
|
||||
|
||||
if (!showPopup) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Overlay show placement="bottom" target={target} onHide={closePopup}>
|
||||
<Popover className="old-editor-warning-tooltip">
|
||||
<Popover.Header>
|
||||
{t('support_for_the_old_editor_is_ending_soon')}
|
||||
<Close variant="dark" onDismiss={closePopup} />
|
||||
</Popover.Header>
|
||||
<Popover.Body>
|
||||
<div>
|
||||
{t(
|
||||
'we_recommend_switching_to_the_new_editor_design_now_so_you_have_time_to_get_to_know_it'
|
||||
)}
|
||||
</div>
|
||||
<OLButton
|
||||
className="old-editor-warning-tooltip-switch-button"
|
||||
isLoading={loading}
|
||||
loadingLabel={t('loading')}
|
||||
onClick={onSwitch}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
{t('switch_to_new_editor_design')}
|
||||
</OLButton>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
+11
-2
@@ -1,4 +1,5 @@
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
@@ -6,12 +7,20 @@ export const useSwitchEnableNewEditorState = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const { setUserSettings } = useUserSettingsContext()
|
||||
const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out')
|
||||
|
||||
const setEditorRedesignStatus = useCallback(
|
||||
(status: boolean): Promise<void> => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
return new Promise((resolve, reject) => {
|
||||
postJSON('/user/settings', { body: { enableNewEditor: status } })
|
||||
postJSON(
|
||||
// Ensure that feature flag overrides are preserved in the request
|
||||
`/user/settings?editor-redesign-opt-out=${isNewEditorOptOutStage ? 'enabled' : 'default'}`,
|
||||
{
|
||||
body: { enableNewEditor: status },
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
setUserSettings(current => ({
|
||||
...current,
|
||||
@@ -28,7 +37,7 @@ export const useSwitchEnableNewEditorState = () => {
|
||||
})
|
||||
})
|
||||
},
|
||||
[setUserSettings]
|
||||
[setUserSettings, isNewEditorOptOutStage]
|
||||
)
|
||||
return { loading, error, setEditorRedesignStatus }
|
||||
}
|
||||
|
||||
@@ -70,3 +70,10 @@ export const useIsNewEditorEnabled = () => {
|
||||
const enabled = userSettings.enableNewEditor
|
||||
return hasAccess && enabled
|
||||
}
|
||||
|
||||
export const useIsNewToNewEditor = () => {
|
||||
const { userSettings } = useUserSettingsContext()
|
||||
const newEditor = useIsNewEditorEnabled()
|
||||
|
||||
return newEditor && !userSettings.enableNewEditorLegacy
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const defaultSettings: UserSettings = {
|
||||
mathPreview: true,
|
||||
referencesSearchMode: 'advanced',
|
||||
enableNewEditor: true,
|
||||
enableNewEditorLegacy: true,
|
||||
breadcrumbs: true,
|
||||
darkModePdf: false,
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
@import 'editor/editor-survey';
|
||||
@import 'editor/editor-tour-tooltip';
|
||||
@import 'editor/new-editor-promo-modal';
|
||||
@import 'editor/old-editor-warning-tooltip';
|
||||
@import 'error-pages';
|
||||
@import 'website-redesign';
|
||||
@import 'group-settings';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.old-editor-warning-tooltip {
|
||||
.popover-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-06);
|
||||
}
|
||||
}
|
||||
|
||||
.old-editor-warning-tooltip-switch-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
@@ -2271,6 +2271,7 @@
|
||||
"suggestion_applied": "Suggestion applied",
|
||||
"suggests_code_completions_while_typing": "Suggests code completions while typing",
|
||||
"support": "Support",
|
||||
"support_for_the_old_editor_is_ending_soon": "Support for the old editor is ending soon",
|
||||
"support_for_your_browser_is_ending_soon": "Support for your browser is ending soon",
|
||||
"supports_up_to_x_licenses": "Supports up to <0>__count__ licenses</0>",
|
||||
"sure_you_want_to_cancel_plan_change": "Are you sure you want to revert your scheduled plan change? You will remain subscribed to the <0>__planName__</0> plan.",
|
||||
@@ -2282,6 +2283,8 @@
|
||||
"switch_compile_mode_for_faster_draft_compilation": "Switch compile mode for faster draft compilation",
|
||||
"switch_easily_between_your_files_comments_track_changes_and_more": "Switch easily between your files, comments, track changes, and more in the new left-hand menu.",
|
||||
"switch_to_editor": "Switch to editor",
|
||||
"switch_to_new_editor_design": "Switch to new editor design",
|
||||
"switch_to_new_look": "Switch to new look",
|
||||
"switch_to_pdf": "Switch to PDF",
|
||||
"switch_to_personal_email_to_keep_your_accounts_separate": "Switch to a personal email to keep your accounts separate.",
|
||||
"switch_to_standard_plan": "Switch to Standard plan",
|
||||
@@ -2357,6 +2360,7 @@
|
||||
"the_following_folder_already_exists_in_this_project": "The following folder already exists in this project:",
|
||||
"the_following_folder_already_exists_in_this_project_plural": "The following folders already exist in this project:",
|
||||
"the_latex_engine_used_for_compiling": "The LaTeX engine used for compiling",
|
||||
"the_new_and_improved_overleaf_editor_design": "The new and improved __appName__ editor design brings you a cleaner, less cluttered interface to help you focus on what matters—your work.",
|
||||
"the_new_overleaf_editor_info": "__appName__’s new look is here. Disabling this option will switch you back to the old editor design.",
|
||||
"the_next_payment_will_be_collected_on": "The next payment will be collected on <strong>__date__</strong>.",
|
||||
"the_original_text_has_changed": "The original text has changed, so this suggestion can’t be applied",
|
||||
@@ -2703,6 +2707,7 @@
|
||||
"we_got_your_request": "We’ve got your request",
|
||||
"we_logged_you_in": "We have logged you in.",
|
||||
"we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "<0>We may also contact you</0> from time to time by email with a survey, or to see if you would like to participate in other user research initiatives",
|
||||
"we_recommend_switching_to_the_new_editor_design_now_so_you_have_time_to_get_to_know_it": "We recommend switching to the new editor design now, so you have time to get to know it.",
|
||||
"we_sent_code": "We’ve sent you a confirmation code",
|
||||
"we_sent_new_code": "We’ve sent a new code. If it doesn’t arrive, make sure to check your spam and any promotions folders.",
|
||||
"we_will_charge_you_now_for_the_cost_of_your_additional_licenses_based_on_remaining_months": "We’ll charge you now for the cost of your additional licenses based on the remaining months of your current subscription.",
|
||||
|
||||
@@ -8,6 +8,14 @@ vi.mock('../../../../app/src/Features/Errors/Errors.js', () => {
|
||||
return vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
||||
})
|
||||
|
||||
vi.mock('../../../../app/src/infrastructure/Metrics.js', () => ({
|
||||
default: {
|
||||
analyticsQueue: {
|
||||
inc: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('UserController', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.user_id = '323123'
|
||||
@@ -143,6 +151,12 @@ describe('UserController', function () {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.SplitTestHandler = {
|
||||
promises: {
|
||||
getAssignment: sinon.stub().resolves({ variant: 'default' }),
|
||||
},
|
||||
}
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Helpers/UrlHelper', () => ({
|
||||
default: ctx.UrlHelper,
|
||||
}))
|
||||
@@ -239,6 +253,13 @@ describe('UserController', function () {
|
||||
default: ctx.Modules,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/SplitTests/SplitTestHandler.mjs',
|
||||
() => ({
|
||||
default: ctx.SplitTestHandler,
|
||||
})
|
||||
)
|
||||
|
||||
ctx.UserController = (await import(modulePath)).default
|
||||
|
||||
ctx.res = {
|
||||
@@ -565,36 +586,85 @@ describe('UserController', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should set enableNewEditor to true', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: true }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditor.should.equal(true)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
describe('when editor-redesign-opt-out is set to default', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.SplitTestHandler.promises.getAssignment.resolves({
|
||||
variant: 'default',
|
||||
})
|
||||
})
|
||||
|
||||
it('should set enableNewEditor to true', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: true }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditor.should.equal(true)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should set enableNewEditor to false', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: false }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditor.should.equal(false)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep enableNewEditor a boolean', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: 'foobar' }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditor.should.equal(true)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set enableNewEditor to false', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: false }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditor.should.equal(false)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
describe('when editor-redesign-opt-out is set to enabled', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.SplitTestHandler.promises.getAssignment.resolves({
|
||||
variant: 'enabled',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep enableNewEditor a boolean', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: 'foobar' }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditor.should.equal(true)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
it('should set enableNewEditorStageFour to true', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: true }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditorStageFour.should.equal(true)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should set enableNewEditorStageFour to false', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: false }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditorStageFour.should.equal(false)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep enableNewEditorStageFour a boolean', function (ctx) {
|
||||
return new Promise(resolve => {
|
||||
ctx.req.body = { enableNewEditor: 'foobar' }
|
||||
ctx.res.sendStatus = code => {
|
||||
ctx.user.ace.enableNewEditorStageFour.should.equal(true)
|
||||
resolve()
|
||||
}
|
||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export type UserSettings = {
|
||||
mathPreview: boolean
|
||||
referencesSearchMode: 'advanced' | 'simple'
|
||||
enableNewEditor: boolean
|
||||
enableNewEditorLegacy: boolean
|
||||
breadcrumbs: boolean
|
||||
darkModePdf: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user