mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Merge pull request #29916 from overleaf/dp-cleanup-editor-redesign-opt-out
Cleanup editor-redesign-opt-out feature flag GitOrigin-RevId: 6a1dc6908fa6276181f9e3996e0b14cfb7537786
This commit is contained in:
@@ -459,7 +459,6 @@ const _ProjectController = {
|
|||||||
'wf-citations-checker-on-selection',
|
'wf-citations-checker-on-selection',
|
||||||
'writefull-asymetric-queue-size-per-model',
|
'writefull-asymetric-queue-size-per-model',
|
||||||
'pdf-dark-mode',
|
'pdf-dark-mode',
|
||||||
'editor-redesign-opt-out',
|
|
||||||
'email-notifications',
|
'email-notifications',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,6 @@ async function buildUserSettings(req, res, user) {
|
|||||||
const enableNewEditorLegacy =
|
const enableNewEditorLegacy =
|
||||||
user.ace.enableNewEditor ?? defaultLegacyEnableNewEditor
|
user.ace.enableNewEditor ?? defaultLegacyEnableNewEditor
|
||||||
|
|
||||||
const assignment = await SplitTestHandler.promises.getAssignment(
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
'editor-redesign-opt-out'
|
|
||||||
)
|
|
||||||
const isOptOutEnabled = assignment.variant === 'enabled'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: user.ace.mode,
|
mode: user.ace.mode,
|
||||||
editorTheme: user.ace.theme,
|
editorTheme: user.ace.theme,
|
||||||
@@ -64,9 +57,7 @@ async function buildUserSettings(req, res, user) {
|
|||||||
mathPreview: user.ace.mathPreview,
|
mathPreview: user.ace.mathPreview,
|
||||||
breadcrumbs: user.ace.breadcrumbs,
|
breadcrumbs: user.ace.breadcrumbs,
|
||||||
referencesSearchMode: user.ace.referencesSearchMode,
|
referencesSearchMode: user.ace.referencesSearchMode,
|
||||||
enableNewEditor: isOptOutEnabled
|
enableNewEditor: enableNewEditorStageFour,
|
||||||
? enableNewEditorStageFour
|
|
||||||
: enableNewEditorLegacy,
|
|
||||||
enableNewEditorLegacy,
|
enableNewEditorLegacy,
|
||||||
darkModePdf: user.ace.darkModePdf ?? false,
|
darkModePdf: user.ace.darkModePdf ?? false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { expressify } from '@overleaf/promise-utils'
|
|||||||
import { acceptsJson } from '../../infrastructure/RequestContentTypeDetection.mjs'
|
import { acceptsJson } from '../../infrastructure/RequestContentTypeDetection.mjs'
|
||||||
import Modules from '../../infrastructure/Modules.mjs'
|
import Modules from '../../infrastructure/Modules.mjs'
|
||||||
import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.mjs'
|
import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.mjs'
|
||||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
|
||||||
|
|
||||||
async function _sendSecurityAlertClearedSessions(user) {
|
async function _sendSecurityAlertClearedSessions(user) {
|
||||||
const emailOptions = {
|
const emailOptions = {
|
||||||
@@ -412,18 +411,7 @@ async function updateUserSettings(req, res, next) {
|
|||||||
user.ace.referencesSearchMode = mode
|
user.ace.referencesSearchMode = mode
|
||||||
}
|
}
|
||||||
if (body.enableNewEditor != null) {
|
if (body.enableNewEditor != null) {
|
||||||
const assignment = await SplitTestHandler.promises.getAssignment(
|
user.ace.enableNewEditorStageFour = Boolean(body.enableNewEditor)
|
||||||
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) {
|
if (body.darkModePdf != null) {
|
||||||
user.ace.darkModePdf = Boolean(body.darkModePdf)
|
user.ace.darkModePdf = Boolean(body.darkModePdf)
|
||||||
|
|||||||
@@ -184,7 +184,6 @@
|
|||||||
"back_to_subscription": "",
|
"back_to_subscription": "",
|
||||||
"back_to_your_projects": "",
|
"back_to_your_projects": "",
|
||||||
"basic_compile_time": "",
|
"basic_compile_time": "",
|
||||||
"be_one_of_the_first_to_try_out_the_new_and_improved_overleaf_editor": "",
|
|
||||||
"before_you_use_error_assistant": "",
|
"before_you_use_error_assistant": "",
|
||||||
"beta_program_already_participating": "",
|
"beta_program_already_participating": "",
|
||||||
"beta_program_benefits": "",
|
"beta_program_benefits": "",
|
||||||
@@ -1235,7 +1234,6 @@
|
|||||||
"overleaf_labs": "",
|
"overleaf_labs": "",
|
||||||
"overleaf_logo": "",
|
"overleaf_logo": "",
|
||||||
"overleafs_functionality_meets_my_needs": "",
|
"overleafs_functionality_meets_my_needs": "",
|
||||||
"overleafs_new_look_is_here": "",
|
|
||||||
"overview": "",
|
"overview": "",
|
||||||
"overwrite": "",
|
"overwrite": "",
|
||||||
"overwriting_the_original_folder": "",
|
"overwriting_the_original_folder": "",
|
||||||
@@ -2044,12 +2042,9 @@
|
|||||||
"try_for_free": "",
|
"try_for_free": "",
|
||||||
"try_it_for_free": "",
|
"try_it_for_free": "",
|
||||||
"try_now": "",
|
"try_now": "",
|
||||||
"try_out_the_new_editor_now": "",
|
|
||||||
"try_premium_for_free": "",
|
"try_premium_for_free": "",
|
||||||
"try_recompile_project_or_troubleshoot": "",
|
"try_recompile_project_or_troubleshoot": "",
|
||||||
"try_relinking_provider": "",
|
"try_relinking_provider": "",
|
||||||
"try_the_new_editor_design": "",
|
|
||||||
"try_the_new_look": "",
|
|
||||||
"try_to_compile_despite_errors": "",
|
"try_to_compile_despite_errors": "",
|
||||||
"turn_off": "",
|
"turn_off": "",
|
||||||
"turn_off_link_sharing": "",
|
"turn_off_link_sharing": "",
|
||||||
|
|||||||
+2
-6
@@ -4,14 +4,12 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useSwitchEnableNewEditorState } from '../ide-redesign/hooks/use-switch-enable-new-editor-state'
|
import { useSwitchEnableNewEditorState } from '../ide-redesign/hooks/use-switch-enable-new-editor-state'
|
||||||
import MaterialIcon from '@/shared/components/material-icon'
|
import MaterialIcon from '@/shared/components/material-icon'
|
||||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
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'
|
import OldEditorWarningTooltip from '../ide-redesign/components/old-editor-warning-tooltip'
|
||||||
|
|
||||||
const TryNewEditorButton = () => {
|
const TryNewEditorButton = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState()
|
const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState()
|
||||||
const { sendEvent } = useEditorAnalytics()
|
const { sendEvent } = useEditorAnalytics()
|
||||||
const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out')
|
|
||||||
const [buttonElt, setButtonElt] = useState<HTMLButtonElement | null>(null)
|
const [buttonElt, setButtonElt] = useState<HTMLButtonElement | null>(null)
|
||||||
const buttonRef = useCallback((node: HTMLButtonElement) => {
|
const buttonRef = useCallback((node: HTMLButtonElement) => {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
@@ -37,11 +35,9 @@ const TryNewEditorButton = () => {
|
|||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
>
|
>
|
||||||
<MaterialIcon type="fiber_new" />
|
<MaterialIcon type="fiber_new" />
|
||||||
{isNewEditorOptOutStage
|
{t('switch_to_new_look')}
|
||||||
? t('switch_to_new_look')
|
|
||||||
: t('try_the_new_editor_design')}
|
|
||||||
</OLButton>
|
</OLButton>
|
||||||
{isNewEditorOptOutStage && <OldEditorWarningTooltip target={buttonElt} />}
|
<OldEditorWarningTooltip target={buttonElt} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,15 @@ import { memo } from 'react'
|
|||||||
import ForceDisconnected from '@/features/ide-react/components/modals/force-disconnected'
|
import ForceDisconnected from '@/features/ide-react/components/modals/force-disconnected'
|
||||||
import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsaved-docs'
|
import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsaved-docs'
|
||||||
import SystemMessages from '@/shared/components/system-messages'
|
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 NewEditorOptOutIntroModal from '@/features/ide-redesign/components/new-editor-opt-out-intro-modal'
|
||||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
|
||||||
|
|
||||||
export const Modals = memo(() => {
|
export const Modals = memo(() => {
|
||||||
const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ForceDisconnected />
|
<ForceDisconnected />
|
||||||
<UnsavedDocs />
|
<UnsavedDocs />
|
||||||
<SystemMessages />
|
<SystemMessages />
|
||||||
{isNewEditorOptOutStage ? (
|
<NewEditorOptOutIntroModal />
|
||||||
<NewEditorOptOutIntroModal />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<NewEditorPromoModal />
|
|
||||||
<NewEditorIntroModal />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
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 { useIsNewEditorEnabledAsExistingUser } 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'
|
|
||||||
|
|
||||||
export default function NewEditorIntroModal() {
|
|
||||||
const { inactiveTutorials } = useEditorContext()
|
|
||||||
const {
|
|
||||||
tryShowingPopup,
|
|
||||||
showPopup: showModal,
|
|
||||||
dismissTutorial,
|
|
||||||
completeTutorial,
|
|
||||||
clearPopup,
|
|
||||||
} = useTutorial(TUTORIAL_KEY, {
|
|
||||||
name: TUTORIAL_KEY,
|
|
||||||
})
|
|
||||||
const { startTour } = useNewEditorTourContext()
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const canShow = useIsNewEditorEnabledAsExistingUser()
|
|
||||||
const [hasShown, setHasShown] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (canShow && !hasShown && !inactiveTutorials.includes(TUTORIAL_KEY)) {
|
|
||||||
tryShowingPopup('notification-prompt')
|
|
||||||
setHasShown(true)
|
|
||||||
}
|
|
||||||
}, [tryShowingPopup, inactiveTutorials, canShow, hasShown])
|
|
||||||
|
|
||||||
const startProductTour = useCallback(() => {
|
|
||||||
completeTutorial({ event: 'notification-click', action: 'complete' })
|
|
||||||
startTour()
|
|
||||||
clearPopup()
|
|
||||||
}, [completeTutorial, startTour, clearPopup])
|
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
|
||||||
dismissTutorial('notification-dismiss')
|
|
||||||
clearPopup()
|
|
||||||
}, [dismissTutorial, clearPopup])
|
|
||||||
|
|
||||||
if (!canShow) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OLModal show={showModal} onHide={closeModal}>
|
|
||||||
<OLModalHeader>
|
|
||||||
<OLModalTitle>{t('introducing_overleafs_new_look')}</OLModalTitle>
|
|
||||||
</OLModalHeader>
|
|
||||||
<OLModalBody className="new-editor-intro-modal-body">
|
|
||||||
{/* 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import {
|
|
||||||
OLModal,
|
|
||||||
OLModalBody,
|
|
||||||
OLModalFooter,
|
|
||||||
OLModalHeader,
|
|
||||||
OLModalTitle,
|
|
||||||
} from '@/shared/components/ol/ol-modal'
|
|
||||||
import { useSwitchEnableNewEditorState } from '../hooks/use-switch-enable-new-editor-state'
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
|
||||||
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
|
|
||||||
import OLButton from '@/shared/components/ol/ol-button'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
|
||||||
import { useEditorContext } from '@/shared/context/editor-context'
|
|
||||||
import {
|
|
||||||
canUseNewEditorAsExistingUser,
|
|
||||||
useIsNewEditorEnabled,
|
|
||||||
} from '../utils/new-editor-utils'
|
|
||||||
import promoVideo from './new-editor-promo-video.mp4'
|
|
||||||
|
|
||||||
const TUTORIAL_KEY = 'new-editor-opt-in'
|
|
||||||
|
|
||||||
export default function NewEditorPromoModal() {
|
|
||||||
const { inactiveTutorials } = useEditorContext()
|
|
||||||
const {
|
|
||||||
tryShowingPopup,
|
|
||||||
showPopup: showModal,
|
|
||||||
dismissTutorial,
|
|
||||||
completeTutorial,
|
|
||||||
clearPopup,
|
|
||||||
} = useTutorial(TUTORIAL_KEY, {
|
|
||||||
name: TUTORIAL_KEY,
|
|
||||||
})
|
|
||||||
const { setEditorRedesignStatus } = useSwitchEnableNewEditorState()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const newEditor = useIsNewEditorEnabled()
|
|
||||||
const canShow = canUseNewEditorAsExistingUser() && !newEditor
|
|
||||||
const [hasShown, setHasShown] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (canShow && !hasShown && !inactiveTutorials.includes(TUTORIAL_KEY)) {
|
|
||||||
tryShowingPopup('notification-prompt')
|
|
||||||
setHasShown(true)
|
|
||||||
}
|
|
||||||
}, [tryShowingPopup, inactiveTutorials, canShow, hasShown])
|
|
||||||
|
|
||||||
const switchToNewEditor = useCallback(() => {
|
|
||||||
setEditorRedesignStatus(true)
|
|
||||||
completeTutorial({ event: 'notification-click', action: 'complete' })
|
|
||||||
clearPopup()
|
|
||||||
}, [setEditorRedesignStatus, completeTutorial, clearPopup])
|
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
|
||||||
dismissTutorial('notification-dismiss')
|
|
||||||
clearPopup()
|
|
||||||
}, [dismissTutorial, clearPopup])
|
|
||||||
|
|
||||||
if (!canShow) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OLModal show={showModal} onHide={closeModal}>
|
|
||||||
<OLModalHeader>
|
|
||||||
<OLModalTitle>{t('overleafs_new_look_is_here')}</OLModalTitle>
|
|
||||||
</OLModalHeader>
|
|
||||||
<OLModalBody className="new-editor-promo-modal-body">
|
|
||||||
<div>
|
|
||||||
{t(
|
|
||||||
'be_one_of_the_first_to_try_out_the_new_and_improved_overleaf_editor'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
|
||||||
<video autoPlay loop muted>
|
|
||||||
<source src={promoVideo} type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
<div>
|
|
||||||
<Trans
|
|
||||||
i18nKey="try_out_the_new_editor_now"
|
|
||||||
components={[
|
|
||||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
|
||||||
<a
|
|
||||||
href="https://www.overleaf.com/blog/introducing-overleafs-new-look"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
key="link"
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</OLModalBody>
|
|
||||||
<OLModalFooter>
|
|
||||||
<OLButton onClick={closeModal} variant="secondary">
|
|
||||||
{t('not_now')}
|
|
||||||
</OLButton>
|
|
||||||
<OLButton onClick={switchToNewEditor} variant="primary">
|
|
||||||
{t('try_the_new_look')}
|
|
||||||
</OLButton>
|
|
||||||
</OLModalFooter>
|
|
||||||
</OLModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+4
-10
@@ -1,5 +1,4 @@
|
|||||||
import { postJSON } from '@/infrastructure/fetch-json'
|
import { postJSON } from '@/infrastructure/fetch-json'
|
||||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
|
||||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
@@ -7,20 +6,15 @@ export const useSwitchEnableNewEditorState = () => {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const { setUserSettings } = useUserSettingsContext()
|
const { setUserSettings } = useUserSettingsContext()
|
||||||
const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out')
|
|
||||||
|
|
||||||
const setEditorRedesignStatus = useCallback(
|
const setEditorRedesignStatus = useCallback(
|
||||||
(status: boolean): Promise<void> => {
|
(status: boolean): Promise<void> => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
postJSON(
|
postJSON('/user/settings', {
|
||||||
// Ensure that feature flag overrides are preserved in the request
|
body: { enableNewEditor: status },
|
||||||
`/user/settings?editor-redesign-opt-out=${isNewEditorOptOutStage ? 'enabled' : 'default'}`,
|
})
|
||||||
{
|
|
||||||
body: { enableNewEditor: status },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setUserSettings(current => ({
|
setUserSettings(current => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -37,7 +31,7 @@ export const useSwitchEnableNewEditorState = () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[setUserSettings, isNewEditorOptOutStage]
|
[setUserSettings]
|
||||||
)
|
)
|
||||||
return { loading, error, setEditorRedesignStatus }
|
return { loading, error, setEditorRedesignStatus }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,16 @@
|
|||||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||||
import getMeta from '@/utils/meta'
|
import getMeta from '@/utils/meta'
|
||||||
import { isSplitTestEnabled, getSplitTestVariant } from '@/utils/splitTestUtils'
|
|
||||||
|
|
||||||
const ignoringUserCutoffDate =
|
// For e2e tests purposes, allow overriding to old editor
|
||||||
new URLSearchParams(window.location.search).get('skip-new-user-check') ===
|
export const oldEditorOverride =
|
||||||
'true'
|
new URLSearchParams(window.location.search).get('old-editor-override') ===
|
||||||
|
|
||||||
// For E2E tests, allow forcing a user to be treated as an existing user
|
|
||||||
const existingUserOverride =
|
|
||||||
new URLSearchParams(window.location.search).get('existing-user-override') ===
|
|
||||||
'true'
|
'true'
|
||||||
|
|
||||||
// We don't want to enable the new editor on server-pro/CE until we have fully rolled it out on SaaS
|
// We don't want to enable the new editor on server-pro/CE until we have fully rolled it out on SaaS
|
||||||
const { isOverleaf } = getMeta('ol-ExposedSettings')
|
const { isOverleaf } = getMeta('ol-ExposedSettings')
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
export const isNewUser = () => {
|
|
||||||
if (existingUserOverride) return false
|
|
||||||
|
|
||||||
if (ignoringUserCutoffDate) return true
|
|
||||||
const user = getMeta('ol-user')
|
|
||||||
|
|
||||||
if (!user.signUpDate) return false
|
|
||||||
|
|
||||||
const createdAt = new Date(user.signUpDate)
|
|
||||||
return createdAt > NEW_USER_CUTOFF_DATE
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isSplitTestUser = () => {
|
|
||||||
if (existingUserOverride) return false
|
|
||||||
|
|
||||||
const user = getMeta('ol-user')
|
|
||||||
if (!user.signUpDate) return false
|
|
||||||
|
|
||||||
const createdAt = new Date(user.signUpDate)
|
|
||||||
return (
|
|
||||||
createdAt > SPLIT_TEST_USER_CUTOFF_DATE && createdAt <= NEW_USER_CUTOFF_DATE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const canUseNewEditorAsExistingUser = () => {
|
|
||||||
return !canUseNewEditorAsNewUser() && isSplitTestEnabled('editor-redesign')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const canUseNewEditorAsNewUser = () => {
|
|
||||||
const newUserTestVariant = getSplitTestVariant('editor-redesign-new-users')
|
|
||||||
return (
|
|
||||||
isOverleaf &&
|
|
||||||
(isNewUser() || (isSplitTestUser() && newUserTestVariant !== 'default'))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const canUseNewEditor = () => {
|
export const canUseNewEditor = () => {
|
||||||
return canUseNewEditorAsExistingUser() || canUseNewEditorAsNewUser()
|
return isOverleaf && !oldEditorOverride
|
||||||
}
|
|
||||||
|
|
||||||
export const useIsNewEditorEnabledAsExistingUser = () => {
|
|
||||||
const { userSettings } = useUserSettingsContext()
|
|
||||||
const hasAccess = canUseNewEditorAsExistingUser()
|
|
||||||
const enabled = userSettings.enableNewEditor
|
|
||||||
return hasAccess && enabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useIsNewEditorEnabled = () => {
|
export const useIsNewEditorEnabled = () => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
.new-editor-promo-modal-body,
|
|
||||||
.new-editor-intro-modal-body {
|
.new-editor-intro-modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -233,7 +233,6 @@
|
|||||||
"basic": "Basic",
|
"basic": "Basic",
|
||||||
"basic_compile_time": "Basic compile time",
|
"basic_compile_time": "Basic compile time",
|
||||||
"basic_compile_timeout_on_fast_servers": "Basic compile timeout on fast servers",
|
"basic_compile_timeout_on_fast_servers": "Basic compile timeout on fast servers",
|
||||||
"be_one_of_the_first_to_try_out_the_new_and_improved_overleaf_editor": "Be one of the first to try out the improved __appName__ editor design, bringing you a cleaner, less cluttered interface to help you focus on what matters—your work.",
|
|
||||||
"before_you_use_error_assistant": "Before you use Error Assist",
|
"before_you_use_error_assistant": "Before you use Error Assist",
|
||||||
"beta": "Beta",
|
"beta": "Beta",
|
||||||
"beta_feature_badge": "Beta feature badge",
|
"beta_feature_badge": "Beta feature badge",
|
||||||
@@ -1615,7 +1614,6 @@
|
|||||||
"overleaf_plans_and_pricing": "overleaf plans and pricing",
|
"overleaf_plans_and_pricing": "overleaf plans and pricing",
|
||||||
"overleaf_template_gallery": "overleaf template gallery",
|
"overleaf_template_gallery": "overleaf template gallery",
|
||||||
"overleafs_functionality_meets_my_needs": "Overleaf’s functionality meets my needs.",
|
"overleafs_functionality_meets_my_needs": "Overleaf’s functionality meets my needs.",
|
||||||
"overleafs_new_look_is_here": "__appName__’s new look is here",
|
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"overwrite": "Overwrite",
|
"overwrite": "Overwrite",
|
||||||
"overwriting_the_original_folder": "Overwriting the original folder will delete it and all the files it contains.",
|
"overwriting_the_original_folder": "Overwriting the original folder will delete it and all the files it contains.",
|
||||||
@@ -2576,12 +2574,9 @@
|
|||||||
"try_for_free": "Try for free",
|
"try_for_free": "Try for free",
|
||||||
"try_it_for_free": "Try it for free",
|
"try_it_for_free": "Try it for free",
|
||||||
"try_now": "Try Now",
|
"try_now": "Try Now",
|
||||||
"try_out_the_new_editor_now": "Try out the new design now (you can switch back at any time), or <0>read more about the changes we’re making</0>.",
|
|
||||||
"try_premium_for_free": "Try Premium for free",
|
"try_premium_for_free": "Try Premium for free",
|
||||||
"try_recompile_project_or_troubleshoot": "Please try recompiling the project from scratch, and if that doesn’t help, follow our <0>troubleshooting guide</0>.",
|
"try_recompile_project_or_troubleshoot": "Please try recompiling the project from scratch, and if that doesn’t help, follow our <0>troubleshooting guide</0>.",
|
||||||
"try_relinking_provider": "It looks like you need to re-link your __provider__ account.",
|
"try_relinking_provider": "It looks like you need to re-link your __provider__ account.",
|
||||||
"try_the_new_editor_design": "Try the new editor design",
|
|
||||||
"try_the_new_look": "Try the new look",
|
|
||||||
"try_to_compile_despite_errors": "Try to compile despite errors",
|
"try_to_compile_despite_errors": "Try to compile despite errors",
|
||||||
"turn_off": "Turn off",
|
"turn_off": "Turn off",
|
||||||
"turn_off_link_sharing": "Turn off link sharing",
|
"turn_off_link_sharing": "Turn off link sharing",
|
||||||
|
|||||||
@@ -8,14 +8,6 @@ vi.mock('../../../../app/src/Features/Errors/Errors.js', () => {
|
|||||||
return vi.importActual('../../../../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 () {
|
describe('UserController', function () {
|
||||||
beforeEach(async function (ctx) {
|
beforeEach(async function (ctx) {
|
||||||
ctx.user_id = '323123'
|
ctx.user_id = '323123'
|
||||||
@@ -151,12 +143,6 @@ describe('UserController', function () {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SplitTestHandler = {
|
|
||||||
promises: {
|
|
||||||
getAssignment: sinon.stub().resolves({ variant: 'default' }),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.doMock('../../../../app/src/Features/Helpers/UrlHelper', () => ({
|
vi.doMock('../../../../app/src/Features/Helpers/UrlHelper', () => ({
|
||||||
default: ctx.UrlHelper,
|
default: ctx.UrlHelper,
|
||||||
}))
|
}))
|
||||||
@@ -253,13 +239,6 @@ describe('UserController', function () {
|
|||||||
default: ctx.Modules,
|
default: ctx.Modules,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock(
|
|
||||||
'../../../../app/src/Features/SplitTests/SplitTestHandler.mjs',
|
|
||||||
() => ({
|
|
||||||
default: ctx.SplitTestHandler,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.UserController = (await import(modulePath)).default
|
ctx.UserController = (await import(modulePath)).default
|
||||||
|
|
||||||
ctx.res = {
|
ctx.res = {
|
||||||
@@ -586,85 +565,36 @@ describe('UserController', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when editor-redesign-opt-out is set to default', function () {
|
it('should set enableNewEditorStageFour to true', function (ctx) {
|
||||||
beforeEach(function (ctx) {
|
return new Promise(resolve => {
|
||||||
ctx.SplitTestHandler.promises.getAssignment.resolves({
|
ctx.req.body = { enableNewEditor: true }
|
||||||
variant: 'default',
|
ctx.res.sendStatus = code => {
|
||||||
})
|
ctx.user.ace.enableNewEditorStageFour.should.equal(true)
|
||||||
})
|
resolve()
|
||||||
|
}
|
||||||
it('should set enableNewEditor to true', function (ctx) {
|
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when editor-redesign-opt-out is set to enabled', function () {
|
it('should set enableNewEditorStageFour to false', function (ctx) {
|
||||||
beforeEach(function (ctx) {
|
return new Promise(resolve => {
|
||||||
ctx.SplitTestHandler.promises.getAssignment.resolves({
|
ctx.req.body = { enableNewEditor: false }
|
||||||
variant: 'enabled',
|
ctx.res.sendStatus = code => {
|
||||||
})
|
ctx.user.ace.enableNewEditorStageFour.should.equal(false)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should set enableNewEditorStageFour to true', function (ctx) {
|
it('should keep enableNewEditorStageFour a boolean', function (ctx) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
ctx.req.body = { enableNewEditor: true }
|
ctx.req.body = { enableNewEditor: 'foobar' }
|
||||||
ctx.res.sendStatus = code => {
|
ctx.res.sendStatus = code => {
|
||||||
ctx.user.ace.enableNewEditorStageFour.should.equal(true)
|
ctx.user.ace.enableNewEditorStageFour.should.equal(true)
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
ctx.UserController.updateUserSettings(ctx.req, ctx.res)
|
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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user