mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
Merge pull request #30393 from overleaf/dp-test-revert-2
Revert "Merge pull request #29916 from overleaf/dp-cleanup-editor-red… GitOrigin-RevId: c2f14fb55e74a1fcb026e37822774724c36bc0dc
This commit is contained in:
@@ -184,6 +184,7 @@
|
||||
"back_to_subscription": "",
|
||||
"back_to_your_projects": "",
|
||||
"basic_compile_time": "",
|
||||
"be_one_of_the_first_to_try_out_the_new_and_improved_overleaf_editor": "",
|
||||
"before_you_use_error_assistant": "",
|
||||
"beta_program_already_participating": "",
|
||||
"beta_program_benefits": "",
|
||||
@@ -1234,6 +1235,7 @@
|
||||
"overleaf_labs": "",
|
||||
"overleaf_logo": "",
|
||||
"overleafs_functionality_meets_my_needs": "",
|
||||
"overleafs_new_look_is_here": "",
|
||||
"overview": "",
|
||||
"overwrite": "",
|
||||
"overwriting_the_original_folder": "",
|
||||
@@ -2042,9 +2044,12 @@
|
||||
"try_for_free": "",
|
||||
"try_it_for_free": "",
|
||||
"try_now": "",
|
||||
"try_out_the_new_editor_now": "",
|
||||
"try_premium_for_free": "",
|
||||
"try_recompile_project_or_troubleshoot": "",
|
||||
"try_relinking_provider": "",
|
||||
"try_the_new_editor_design": "",
|
||||
"try_the_new_look": "",
|
||||
"try_to_compile_despite_errors": "",
|
||||
"turn_off": "",
|
||||
"turn_off_link_sharing": "",
|
||||
|
||||
+6
-2
@@ -4,12 +4,14 @@ 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) {
|
||||
@@ -35,9 +37,11 @@ const TryNewEditorButton = () => {
|
||||
ref={buttonRef}
|
||||
>
|
||||
<MaterialIcon type="fiber_new" />
|
||||
{t('switch_to_new_look')}
|
||||
{isNewEditorOptOutStage
|
||||
? t('switch_to_new_look')
|
||||
: t('try_the_new_editor_design')}
|
||||
</OLButton>
|
||||
<OldEditorWarningTooltip target={buttonElt} />
|
||||
{isNewEditorOptOutStage && <OldEditorWarningTooltip target={buttonElt} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,15 +2,27 @@ import { memo } from 'react'
|
||||
import ForceDisconnected from '@/features/ide-react/components/modals/force-disconnected'
|
||||
import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsaved-docs'
|
||||
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 />
|
||||
<NewEditorOptOutIntroModal />
|
||||
{isNewEditorOptOutStage ? (
|
||||
<NewEditorOptOutIntroModal />
|
||||
) : (
|
||||
<>
|
||||
<NewEditorPromoModal />
|
||||
<NewEditorIntroModal />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
+10
-4
@@ -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,15 +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,
|
||||
@@ -31,7 +37,7 @@ export const useSwitchEnableNewEditorState = () => {
|
||||
})
|
||||
})
|
||||
},
|
||||
[setUserSettings]
|
||||
[setUserSettings, isNewEditorOptOutStage]
|
||||
)
|
||||
return { loading, error, setEditorRedesignStatus }
|
||||
}
|
||||
|
||||
@@ -1,16 +1,67 @@
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { isSplitTestEnabled, getSplitTestVariant } from '@/utils/splitTestUtils'
|
||||
|
||||
// For e2e tests purposes, allow overriding to old editor
|
||||
export const oldEditorOverride =
|
||||
new URLSearchParams(window.location.search).get('old-editor-override') ===
|
||||
const ignoringUserCutoffDate =
|
||||
new URLSearchParams(window.location.search).get('skip-new-user-check') ===
|
||||
'true'
|
||||
|
||||
// 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'
|
||||
|
||||
// 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 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 = () => {
|
||||
return isOverleaf && !oldEditorOverride
|
||||
return canUseNewEditorAsExistingUser() || canUseNewEditorAsNewUser()
|
||||
}
|
||||
|
||||
export const useIsNewEditorEnabledAsExistingUser = () => {
|
||||
const { userSettings } = useUserSettingsContext()
|
||||
const hasAccess = canUseNewEditorAsExistingUser()
|
||||
const enabled = userSettings.enableNewEditor
|
||||
return hasAccess && enabled
|
||||
}
|
||||
|
||||
export const useIsNewEditorEnabled = () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.new-editor-promo-modal-body,
|
||||
.new-editor-intro-modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user