Merge pull request #29607 from overleaf/dp-redesign-opt-in

Add opt-in for editor redesign

GitOrigin-RevId: 29ec8f4045a6bf29ab26a5ce5bceff70fb3aba6e
This commit is contained in:
David
2025-11-19 09:25:30 +00:00
committed by Copybot
parent 42fa9701e5
commit af41215058
45 changed files with 812 additions and 655 deletions
@@ -864,6 +864,12 @@ const _ProjectController = {
!userHasPremiumSub &&
!userInNonIndividualSub
const userSettings = await UserSettingsHelper.buildUserSettings(
req,
res,
user
)
res.render(template, {
title: project.name,
priority_title: true,
@@ -902,7 +908,7 @@ const _ProjectController = {
isMemberOfGroupSubscription: userIsMemberOfGroupSubscription,
hasInstitutionLicence: userHasInstitutionLicence,
},
userSettings: UserSettingsHelper.buildUserSettings(user),
userSettings,
labsExperiments: user.labsExperiments ?? [],
privilegeLevel,
anonymous,
@@ -542,6 +542,12 @@ async function projectListPage(req, res, next) {
'themed-project-dashboard'
)
const userSettings = await UserSettingsHelper.buildUserSettings(
req,
res,
user
)
res.render('project/list-react', {
title: 'your_projects',
usersBestSubscription,
@@ -550,7 +556,7 @@ async function projectListPage(req, res, next) {
user,
userAffiliations,
userEmails,
userSettings: UserSettingsHelper.buildUserSettings(user),
userSettings,
reconfirmedViaSAML,
allInReconfirmNotificationPeriods,
survey,
@@ -1,4 +1,38 @@
function buildUserSettings(user) {
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
// Copied from services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts
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) {
if (req.query['existing-user-override'] === 'true') {
return false
}
if (req.query['skip-new-user-check'] === 'true') {
return true
}
if (user.signUpDate >= NEW_USER_CUTOFF_DATE) {
return true
}
if (user.signUpDate >= SPLIT_TEST_USER_CUTOFF_DATE) {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'editor-redesign-new-users'
)
return assignment.variant !== 'default'
}
return false
}
async function buildUserSettings(req, res, user) {
const defaultEnableNewEditor = await getEnableNewEditorDefault(req, res, user)
return {
mode: user.ace.mode,
editorTheme: user.ace.theme,
@@ -13,7 +47,7 @@ function buildUserSettings(user) {
mathPreview: user.ace.mathPreview,
breadcrumbs: user.ace.breadcrumbs,
referencesSearchMode: user.ace.referencesSearchMode,
enableNewEditor: user.ace.enableNewEditor ?? true,
enableNewEditor: user.ace.enableNewEditor ?? defaultEnableNewEditor,
}
}
@@ -24,6 +24,8 @@ const VALID_KEYS = [
'rolling-compile-image-changed',
'groups-enterprise-banner',
'groups-enterprise-banner-repeat',
'new-editor-opt-in',
'new-editor-intro',
]
async function completeTutorial(req, res, next) {
@@ -182,12 +182,11 @@
"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": "",
"beta_program_already_participating": "",
"beta_program_benefits": "",
"beta_program_not_participating": "",
"beta_program_the_new_overleaf_editor": "",
"billed_annually_at": "",
"billed_monthly_at": "",
"billed_yearly": "",
@@ -240,6 +239,7 @@
"center": "",
"change": "",
"change_currency": "",
"change_how_you_see_the_editor": "",
"change_language": "",
"change_or_cancel-cancel": "",
"change_or_cancel-change": "",
@@ -581,6 +581,7 @@
"expires": "",
"expires_in_days": "",
"expires_on": "",
"explore_what_s_new": "",
"export_csv": "",
"export_project_to_github": "",
"failed_to_send_group_invite_to_email": "",
@@ -615,11 +616,13 @@
"files_selected": "",
"filter_projects": "",
"find": "",
"find_and_fix_errors_faster": "",
"find_out_more": "",
"find_out_more_about_institution_login": "",
"find_out_more_about_the_file_outline": "",
"find_out_more_nt": "",
"finding_a_fix": "",
"finish": "",
"first_name": "",
"fit_to_height": "",
"fit_to_width": "",
@@ -848,7 +851,6 @@
"imported_from_zotero_at_date": "",
"importing": "",
"importing_and_merging_changes_in_github": "",
"improved_dark_mode": "",
"in_order_to_match_institutional_metadata_2": "",
"in_order_to_match_institutional_metadata_associated": "",
"include_caption": "",
@@ -887,6 +889,7 @@
"integrations": "",
"integrations_like_github": "",
"interested_in_cheaper_personal_plan": "",
"introducing_overleafs_new_look": "",
"invalid_confirmation_code": "",
"invalid_email": "",
"invalid_file_name": "",
@@ -1112,15 +1115,11 @@
"new_compile_domain_notice": "",
"new_compiles_in_this_project_will_automatically_use_the_newest_version": "",
"new_create_tables_and_equations": "",
"new_editor": "",
"new_editor_experience": "",
"new_editor_info": "",
"new_editor_look": "",
"new_error_logs_make_it_easier_to_find_whats_wrong": "",
"new_file": "",
"new_folder": "",
"new_look_and_feel": "",
"new_look_and_placement_of_the_settings": "",
"new_name": "",
"new_navigation_introducing_left_hand_side_rail_and_top_menus": "",
"new_password": "",
"new_project": "",
"new_subscription_will_be_billed_immediately": "",
@@ -1165,6 +1164,7 @@
"not_a_student": "",
"not_managed": "",
"not_now": "",
"not_sure_about_switching_yet": "",
"notification": "",
"notification_personal_and_group_subscriptions": "",
"notification_project_invite_accepted_message": "",
@@ -1217,6 +1217,7 @@
"overleaf_labs": "",
"overleaf_logo": "",
"overleafs_functionality_meets_my_needs": "",
"overleafs_new_look_is_here": "",
"overview": "",
"overwrite": "",
"overwriting_the_original_folder": "",
@@ -1397,6 +1398,7 @@
"read_lines_from_path": "",
"read_more": "",
"read_more_about_managed_users": "",
"read_more_about_the_new_editor": "",
"read_only_dropbox_sync_message": "",
"read_only_token": "",
"read_write_token": "",
@@ -1504,7 +1506,6 @@
"revert_pending_plan_change": "",
"review": "",
"review_panel": "",
"review_panel_and_error_logs_moved_to_the_left": "",
"reviewer": "",
"reviewer_dropbox_sync_message": "",
"reviewing": "",
@@ -1621,6 +1622,7 @@
"setup_another_account_under_a_personal_email_address": "",
"share": "",
"share_feedback": "",
"share_feedback_on_the_new_editor": "",
"share_project": "",
"shared_with_you": "",
"sharelatex_beta_program": "",
@@ -1648,6 +1650,7 @@
"showing_x_results_of_total": "",
"sign_up": "",
"simple_search_mode": "",
"simplified_working_starts_here": "",
"single_sign_on_sso": "",
"size": "",
"something_not_right": "",
@@ -1775,10 +1778,10 @@
"sure_you_want_to_change_plan": "",
"sure_you_want_to_delete": "",
"sure_you_want_to_leave_group": "",
"switch_between_dark_and_light_mode": "",
"switch_compile_mode_for_faster_draft_compilation": "",
"switch_easily_between_your_files_comments_track_changes_and_more": "",
"switch_to_editor": "",
"switch_to_new_editor": "",
"switch_to_old_editor": "",
"switch_to_pdf": "",
"switch_to_personal_email_to_keep_your_accounts_separate": "",
"switch_to_standard_plan": "",
@@ -1836,7 +1839,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_overleaf_editor_try_now_in_beta": "",
"the_new_overleaf_editor_info": "",
"the_next_payment_will_be_collected_on": "",
"the_original_text_has_changed": "",
"the_overleaf_color_scheme": "",
@@ -1867,7 +1870,6 @@
"this_experiment_isnt_accepting_new_participants": "",
"this_field_is_required": "",
"this_grants_access_to_features_2": "",
"this_is_a_beta_release_for_the_new_overleaf_editor": "",
"this_is_a_new_feature": "",
"this_is_the_file_that_references_pulled_from_your_reference_manager_will_be_added_to": "",
"this_organization_is_tax_exempt": "",
@@ -1996,10 +1998,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": "",
"try_the_new_editor_design": "",
"try_the_new_look": "",
"try_to_compile_despite_errors": "",
"turn_off": "",
"turn_off_link_sharing": "",
@@ -2159,13 +2163,12 @@
"well_be_here_when_youre_ready": "",
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
"were_performing_maintenance": "",
"weve_redesigned_our_editor_to_make_it_easier_to_use_and_future_ready": "",
"weve_made_it_easier_to_find_and_use_the_tools_you_need_today": "",
"what_did_you_find_most_helpful": "",
"what_do_you_need_help_with": "",
"what_does_this_mean_for_you": "",
"what_happens_when_sso_is_enabled": "",
"what_should_we_call_you": "",
"whats_different": "",
"when_you_tick_the_include_caption_box": "",
"why_not_pause_instead": "",
"wide": "",
@@ -2215,7 +2218,6 @@
"you_can_select_or_invite_collaborator": "",
"you_can_select_or_invite_collaborator_plural": "",
"you_can_still_use_your_premium_features": "",
"you_can_switch_back_to_the_old_editor_at_any_time": "",
"you_cant_add_or_change_password_due_to_sso": "",
"you_cant_join_this_group_subscription": "",
"you_dont_have_any_add_ons_on_your_account": "",
@@ -4,22 +4,27 @@ import { useSwitchEnableNewEditorState } from '@/features/ide-redesign/hooks/use
import { useLayoutContext } from '@/shared/context/layout-context'
import { useCallback } from 'react'
import {
canUseNewEditorAsNewUser,
canUseNewEditor,
useIsNewEditorEnabled,
} from '@/features/ide-redesign/utils/new-editor-utils'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
export default function SettingsNewEditor() {
const { t } = useTranslation()
const { setEditorRedesignStatus } = useSwitchEnableNewEditorState()
const { setLeftMenuShown } = useLayoutContext()
const enabled = useIsNewEditorEnabled()
const show = canUseNewEditorAsNewUser()
const show = canUseNewEditor()
const { sendEvent } = useEditorAnalytics()
const onChange = useCallback(
(newValue: boolean) => {
sendEvent('switch-to-new-editor', {
location: 'left-menu',
})
setEditorRedesignStatus(newValue).then(() => setLeftMenuShown(false))
},
[setEditorRedesignStatus, setLeftMenuShown]
[setEditorRedesignStatus, setLeftMenuShown, sendEvent]
)
if (!show) {
@@ -40,7 +45,7 @@ export default function SettingsNewEditor() {
label: t('off'),
},
]}
label={t('new_editor')}
label={t('new_editor_look')}
name="new-editor-setting"
/>
)
@@ -1,34 +1,33 @@
import { useCallback } from 'react'
import OLButton from '../../shared/components/ol/ol-button'
import { useIdeRedesignSwitcherContext } from '../ide-react/context/ide-redesign-switcher-context'
import { useTranslation } from 'react-i18next'
import { canUseNewEditorAsExistingUser } from '../ide-redesign/utils/new-editor-utils'
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'
const TryNewEditorButton = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const showModal = canUseNewEditorAsExistingUser()
const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState()
const { sendEvent } = useEditorAnalytics()
const onClick = useCallback(() => {
if (showModal) {
setShowSwitcherModal(true)
} else {
setEditorRedesignStatus(true)
}
}, [setShowSwitcherModal, showModal, setEditorRedesignStatus])
sendEvent('switch-to-new-editor', {
location: 'toolbar',
})
setEditorRedesignStatus(true)
}, [setEditorRedesignStatus, sendEvent])
return (
<div className="d-flex align-items-center">
<OLButton
className="toolbar-experiment-button"
className="toolbar-experiment-button try-new-editor-button"
onClick={onClick}
size="sm"
variant="secondary"
isLoading={loading}
>
{t('try_the_new_editor')}
<MaterialIcon type="fiber_new" />
{t('try_the_new_editor_design')}
</OLButton>
</div>
)
@@ -2,10 +2,8 @@ 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 {
IdeRedesignSwitcherModal,
IdeRedesignIntroModal,
} from '@/features/ide-redesign/components/switcher-modal/beta-modal'
import NewEditorPromoModal from '@/features/ide-redesign/components/new-editor-promo-modal'
import NewEditorIntroModal from '@/features/ide-redesign/components/new-editor-intro-modal'
export const Modals = memo(() => {
return (
@@ -13,8 +11,8 @@ export const Modals = memo(() => {
<ForceDisconnected />
<UnsavedDocs />
<SystemMessages />
<IdeRedesignIntroModal />
<IdeRedesignSwitcherModal />
<NewEditorPromoModal />
<NewEditorIntroModal />
</>
)
})
@@ -1,41 +0,0 @@
import {
createContext,
Dispatch,
FC,
SetStateAction,
useContext,
useState,
} from 'react'
type IdeRedesignSwitcherContextValue = {
showSwitcherModal: boolean
setShowSwitcherModal: Dispatch<SetStateAction<boolean>>
}
export const IdeRedesignSwitcherContext = createContext<
IdeRedesignSwitcherContextValue | undefined
>(undefined)
export const IdeRedesignSwitcherProvider: FC<React.PropsWithChildren> = ({
children,
}) => {
const [showSwitcherModal, setShowSwitcherModal] = useState(false)
return (
<IdeRedesignSwitcherContext.Provider
value={{ showSwitcherModal, setShowSwitcherModal }}
>
{children}
</IdeRedesignSwitcherContext.Provider>
)
}
export const useIdeRedesignSwitcherContext = () => {
const context = useContext(IdeRedesignSwitcherContext)
if (!context) {
throw new Error(
'useIdeRedesignSwitcherContext is only available inside IdeRedesignSwitcherProvider'
)
}
return context
}
@@ -28,8 +28,8 @@ import { SplitTestProvider } from '@/shared/context/split-test-context'
import { UserProvider } from '@/shared/context/user-context'
import { UserFeaturesProvider } from '@/shared/context/user-features-context'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
import { IdeRedesignSwitcherProvider } from './ide-redesign-switcher-context'
import { CommandRegistryProvider } from './command-registry-context'
import { NewEditorTourProvider } from '@/features/ide-redesign/contexts/new-editor-tour-context'
import { EditorSelectionProvider } from '@/shared/context/editor-selection-context'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
@@ -72,9 +72,9 @@ export const ReactContextRoot: FC<
SplitTestProvider,
UserProvider,
UserSettingsProvider,
IdeRedesignSwitcherProvider,
CommandRegistryProvider,
UserFeaturesProvider,
NewEditorTourProvider,
EditorSelectionProvider,
...providers,
}
@@ -111,17 +111,17 @@ export const ReactContextRoot: FC<
<Providers.PermissionsProvider>
<Providers.RailProvider>
<Providers.LayoutProvider>
<Providers.ProjectSettingsProvider>
<Providers.EditorManagerProvider>
<Providers.ReferencesProvider>
<Providers.LocalCompileProvider>
<Providers.DetachCompileProvider>
<Providers.ChatProvider>
<Providers.FileTreeOpenProvider>
<Providers.OnlineUsersProvider>
<Providers.MetadataProvider>
<Providers.OutlineProvider>
<Providers.IdeRedesignSwitcherProvider>
<Providers.NewEditorTourProvider>
<Providers.ProjectSettingsProvider>
<Providers.EditorManagerProvider>
<Providers.ReferencesProvider>
<Providers.LocalCompileProvider>
<Providers.DetachCompileProvider>
<Providers.ChatProvider>
<Providers.FileTreeOpenProvider>
<Providers.OnlineUsersProvider>
<Providers.MetadataProvider>
<Providers.OutlineProvider>
<Providers.CommandRegistryProvider>
<Providers.EditorSelectionProvider>
{
@@ -129,17 +129,17 @@ export const ReactContextRoot: FC<
}
</Providers.EditorSelectionProvider>
</Providers.CommandRegistryProvider>
</Providers.IdeRedesignSwitcherProvider>
</Providers.OutlineProvider>
</Providers.MetadataProvider>
</Providers.OnlineUsersProvider>
</Providers.FileTreeOpenProvider>
</Providers.ChatProvider>
</Providers.DetachCompileProvider>
</Providers.LocalCompileProvider>
</Providers.ReferencesProvider>
</Providers.EditorManagerProvider>
</Providers.ProjectSettingsProvider>
</Providers.OutlineProvider>
</Providers.MetadataProvider>
</Providers.OnlineUsersProvider>
</Providers.FileTreeOpenProvider>
</Providers.ChatProvider>
</Providers.DetachCompileProvider>
</Providers.LocalCompileProvider>
</Providers.ReferencesProvider>
</Providers.EditorManagerProvider>
</Providers.ProjectSettingsProvider>
</Providers.NewEditorTourProvider>
</Providers.LayoutProvider>
</Providers.RailProvider>
</Providers.PermissionsProvider>
@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next'
import EditorTourTooltip from './editor-tour-tooltip'
export default function EditorTourLogsTooltip({
target,
}: {
target: HTMLElement | null
}) {
const { t } = useTranslation()
return (
<EditorTourTooltip
target={target}
placement="bottom"
stage="logs"
header={t('find_and_fix_errors_faster')}
>
{t('new_error_logs_make_it_easier_to_find_whats_wrong')}
</EditorTourTooltip>
)
}
@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next'
import EditorTourTooltip from './editor-tour-tooltip'
export default function EditorTourRailTooltip({
target,
}: {
target: HTMLElement | null
}) {
const { t } = useTranslation()
return (
<EditorTourTooltip
target={target}
placement="right-start"
stage="rail"
header={t('simplified_working_starts_here')}
>
{t('switch_easily_between_your_files_comments_track_changes_and_more')}
</EditorTourTooltip>
)
}
@@ -0,0 +1,33 @@
import { Trans, useTranslation } from 'react-i18next'
import EditorTourTooltip from './editor-tour-tooltip'
export default function EditorTourSwitchBackTooltip({
target,
}: {
target: HTMLElement | null
}) {
const { t } = useTranslation()
return (
<EditorTourTooltip
target={target}
placement="right-end"
stage="switch-back"
header={t('not_sure_about_switching_yet')}
>
<Trans
i18nKey="read_more_about_the_new_editor"
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"
/>,
<strong key="strong" />,
]}
/>
</EditorTourTooltip>
)
}
@@ -0,0 +1,24 @@
import { Trans, useTranslation } from 'react-i18next'
import EditorTourTooltip from './editor-tour-tooltip'
export default function EditorTourThemeTooltip({
target,
}: {
target: HTMLElement | null
}) {
const { t } = useTranslation()
return (
<EditorTourTooltip
target={target}
placement="right-end"
stage="theme"
header={t('switch_between_dark_and_light_mode')}
>
<Trans
i18nKey="change_how_you_see_the_editor"
components={{ strong: <strong /> }}
/>
</EditorTourTooltip>
)
}
@@ -0,0 +1,77 @@
import { Overlay, OverlayProps, Popover } from 'react-bootstrap'
import {
NewEditorTourStage,
useNewEditorTourContext,
} from '../../contexts/new-editor-tour-context'
import Close from '@/shared/components/close'
import OLButton from '@/shared/components/ol/ol-button'
import { useTranslation } from 'react-i18next'
import { useEffect } from 'react'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
export default function EditorTourTooltip({
children,
target,
header,
stage,
placement,
}: {
children: React.ReactNode
target: HTMLElement | null
header: string
stage: NewEditorTourStage
placement?: OverlayProps['placement']
}) {
const { t } = useTranslation()
const {
shouldShowTourStage,
dismissTour,
goToNextStage,
stageNumber,
totalStages,
finishTour,
} = useNewEditorTourContext()
const { sendEvent } = useEditorAnalytics()
const show = shouldShowTourStage(stage)
useEffect(() => {
if (show) {
sendEvent('new-editor-tour-shown', { stage: stageNumber })
}
}, [show, stageNumber, sendEvent])
const isFinalStage = stageNumber === totalStages
if (!show) {
return null
}
return (
<Overlay show placement={placement} target={target} onHide={dismissTour}>
<Popover className="editor-tour-tooltip">
<Popover.Header>
{header}
<Close variant="dark" onDismiss={dismissTour} />
</Popover.Header>
<Popover.Body>
{children}
<div className="editor-tour-tooltip-footer">
<div>
{stageNumber}/{totalStages}
</div>
{isFinalStage ? (
<OLButton onClick={finishTour} variant="link">
{t('finish')}
</OLButton>
) : (
<OLButton onClick={goToNextStage} variant="link">
{t('next')}
</OLButton>
)}
</div>
</Popover.Body>
</Popover>
</Overlay>
)
}
@@ -31,7 +31,6 @@ export default function MainLayout() {
pdfIsOpen: isPdfOpen,
pdfPanelRef,
} = usePdfPane()
const { view, pdfLayout } = useLayoutContext()
const editorIsOpen =
@@ -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>
)
}
@@ -1,4 +1,4 @@
import { memo } from 'react'
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import OLButtonToolbar from '@/shared/components/ol/ol-button-toolbar'
import PdfCompileButton from '@/features/pdf-preview/components/pdf-compile-button'
@@ -6,10 +6,18 @@ import PdfHybridDownloadButton from '@/features/pdf-preview/components/pdf-hybri
import { DetachedSynctexControl } from '@/features/pdf-preview/components/detach-synctex-control'
import SwitchToEditorButton from '@/features/pdf-preview/components/switch-to-editor-button'
import PdfHybridLogsButton from '@/features/pdf-preview/components/pdf-hybrid-logs-button'
import EditorTourLogsTooltip from '../editor-tour/editor-tour-logs-tooltip'
function PdfPreviewHybridToolbar() {
const { t } = useTranslation()
const [logsButtonElt, setLogsButtonElt] = useState<HTMLElement | null>(null)
const logsButtonRef = useCallback((node: HTMLButtonElement) => {
if (node !== null) {
setLogsButtonElt(node)
}
}, [])
// TODO: add detached pdf logic
return (
<OLButtonToolbar
@@ -18,8 +26,9 @@ function PdfPreviewHybridToolbar() {
>
<div className="toolbar-pdf-left">
<PdfCompileButton />
<PdfHybridLogsButton />
<PdfHybridLogsButton ref={logsButtonRef} />
<PdfHybridDownloadButton />
<EditorTourLogsTooltip target={logsButtonElt} />
</div>
<div className="toolbar-pdf-right">
<div className="toolbar-pdf-controls" id="toolbar-pdf-controls" />
@@ -1,12 +1,13 @@
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
import { ReactElement, useCallback } from 'react'
import { forwardRef, ReactElement, useCallback } from 'react'
import {
Dropdown,
DropdownToggle,
} from '@/shared/components/dropdown/dropdown-menu'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { DropdownToggle as BS5DropdownToggle } from 'react-bootstrap'
type RailActionButton = {
key: string
@@ -14,6 +15,7 @@ type RailActionButton = {
title: string
action: () => void
hide?: boolean
ref?: React.Ref<HTMLButtonElement>
}
type RailDropdown = {
@@ -22,66 +24,75 @@ type RailDropdown = {
title: string
dropdown: ReactElement
hide?: boolean
ref?: React.Ref<HTMLButtonElement>
}
export type RailAction = RailDropdown | RailActionButton
export default function RailActionElement({ action }: { action: RailAction }) {
const onActionClick = useCallback(() => {
if ('action' in action) {
action.action()
const RailActionElement = forwardRef<HTMLButtonElement, { action: RailAction }>(
({ action }, ref) => {
const onActionClick = useCallback(() => {
if ('action' in action) {
action.action()
}
}, [action])
if (action.hide) {
return null
}
}, [action])
if (action.hide) {
return null
}
if ('dropdown' in action) {
return (
<Dropdown align="end" drop="end">
if ('dropdown' in action) {
return (
<Dropdown align="end" drop="end">
<OLTooltip
id={`rail-dropdown-tooltip-${action.key}`}
description={action.title}
overlayProps={{ delay: 0, placement: 'right' }}
>
<span>
<DropdownToggle
ref={ref as React.ForwardedRef<typeof BS5DropdownToggle>}
id={`rail-dropdown-btn-${action.key}`}
className="ide-rail-tab-link ide-rail-tab-button ide-rail-tab-dropdown"
as="button"
aria-label={action.title}
>
<MaterialIcon
className="ide-rail-tab-link-icon"
type={action.icon}
unfilled
/>
</DropdownToggle>
</span>
</OLTooltip>
{action.dropdown}
</Dropdown>
)
} else {
return (
<OLTooltip
id={`rail-dropdown-tooltip-${action.key}`}
id={`rail-tab-tooltip-${action.key}`}
description={action.title}
overlayProps={{ delay: 0, placement: 'right' }}
>
<span>
<DropdownToggle
id={`rail-dropdown-btn-${action.key}`}
className="ide-rail-tab-link ide-rail-tab-button ide-rail-tab-dropdown"
as="button"
aria-label={action.title}
>
<MaterialIcon
className="ide-rail-tab-link-icon"
type={action.icon}
unfilled
/>
</DropdownToggle>
</span>
<button
ref={ref}
onClick={onActionClick}
className="ide-rail-tab-link ide-rail-tab-button"
aria-label={action.title}
>
<MaterialIcon
className="ide-rail-tab-link-icon"
type={action.icon}
unfilled
/>
</button>
</OLTooltip>
{action.dropdown}
</Dropdown>
)
} else {
return (
<OLTooltip
id={`rail-tab-tooltip-${action.key}`}
description={action.title}
overlayProps={{ delay: 0, placement: 'right' }}
>
<button
onClick={onActionClick}
className="ide-rail-tab-link ide-rail-tab-button"
aria-label={action.title}
>
<MaterialIcon
className="ide-rail-tab-link-icon"
type={action.icon}
unfilled
/>
</button>
</OLTooltip>
)
)
}
}
}
)
RailActionElement.displayName = 'RailActionElement'
export default RailActionElement
@@ -2,7 +2,6 @@ import getMeta from '@/utils/meta'
import { useTranslation } from 'react-i18next'
import { useRailContext } from '../../contexts/rail-context'
import { useCallback } from 'react'
import { useSurveyUrl } from '../../hooks/use-survey-url'
import {
DropdownDivider,
DropdownItem,
@@ -19,7 +18,6 @@ export default function RailHelpDropdown() {
const openContactUsModal = useCallback(() => {
setActiveModal('contact-us')
}, [setActiveModal])
const surveyURL = useSurveyUrl()
return (
<DropdownMenu>
@@ -40,14 +38,6 @@ export default function RailHelpDropdown() {
{t('contact_us')}
</DropdownItem>
)}
<DropdownItem
href={surveyURL}
role="menuitem"
target="_blank"
rel="noopener noreferrer"
>
{t('give_feedback')}
</DropdownItem>
</DropdownMenu>
)
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Nav, TabContainer } from 'react-bootstrap'
import { useLayoutContext } from '@/shared/context/layout-context'
@@ -26,7 +26,10 @@ import RailResizeHandle from './rail-resize-handle'
import RailModals from './rail-modals'
import RailOverflowDropdown from './rail-overflow-dropdown'
import useRailOverflow from '../../hooks/use-rail-overflow'
import EditorTourRailTooltip from '../editor-tour/editor-tour-rail-tooltip'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import EditorTourThemeTooltip from '../editor-tour/editor-tour-theme-tooltip'
import EditorTourSwitchBackTooltip from '../editor-tour/editor-tour-switch-back-tooltip'
const moduleRailEntries = (
importOverleafModules('railEntries') as {
@@ -47,6 +50,9 @@ export const RailLayout = () => {
const isHistoryView = view === 'history'
const fileTreeRef = useRef<HTMLAnchorElement>(null)
const settingsRef = useRef<HTMLButtonElement>(null)
const railTabs: RailElement[] = useMemo(
() => [
{
@@ -57,6 +63,7 @@ export const RailLayout = () => {
// NOTE: We always need to mount the file tree on first load
// since it is responsible for opening the initial document.
mountOnFirstLoad: true,
ref: fileTreeRef,
},
{
key: 'full-project-search',
@@ -108,6 +115,7 @@ export const RailLayout = () => {
sendEvent('rail-click', { tab: 'settings' })
setLeftMenuShown(true)
},
ref: settingsRef,
},
],
[setLeftMenuShown, t, sendEvent]
@@ -219,7 +227,7 @@ export const RailLayout = () => {
.filter(({ hide }) =>
typeof hide === 'function' ? !hide() : !hide
)
.map(({ icon, key, indicator, title, disabled }) => (
.map(({ icon, key, indicator, title, disabled, ref }) => (
<RailTab
open={isOpen && selectedTab === key}
key={key}
@@ -228,17 +236,25 @@ export const RailLayout = () => {
indicator={indicator}
title={title}
disabled={disabled}
ref={ref}
/>
))}
<RailActionElement key="more-options" action={moreOptionsAction} />
</div>
<nav aria-label={t('help_editor_settings')}>
{railActions.map(action => (
<RailActionElement key={action.key} action={action} />
<RailActionElement
key={action.key}
action={action}
ref={action.ref}
/>
))}
</nav>
</Nav>
</nav>
<EditorTourRailTooltip target={fileTreeRef.current} />
<EditorTourThemeTooltip target={settingsRef.current} />
<EditorTourSwitchBackTooltip target={settingsRef.current} />
<RailPanel
isReviewPanelOpen={isReviewPanelOpen}
isHistoryView={isHistoryView}
@@ -4,26 +4,36 @@ import { useSwitchEnableNewEditorState } from '@/features/ide-redesign/hooks/use
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
import { useCallback } from 'react'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
export default function NewEditorSetting() {
const { t } = useTranslation()
const { setEditorRedesignStatus } = useSwitchEnableNewEditorState()
const { setLeftMenuShown } = useLayoutContext()
const enabled = useIsNewEditorEnabled()
const { sendEvent } = useEditorAnalytics()
const handleToggle = useCallback(() => {
sendEvent('switch-to-old-editor', { location: 'settings-modal' })
setEditorRedesignStatus(!enabled).then(() => setLeftMenuShown(false))
}, [enabled, setEditorRedesignStatus, setLeftMenuShown])
}, [enabled, setEditorRedesignStatus, setLeftMenuShown, sendEvent])
return (
<ToggleSetting
id="new-editor-setting"
label={
<div className="ide-setting-new-editor">
{t('new_editor_experience')}
<div className="ide-setting-beta-tag">{t('beta')}</div>
</div>
label={t('new_editor_look')}
description={
<>
<div>{t('the_new_overleaf_editor_info')}</div>
<a
href="https://forms.gle/3tPYhXcBVGmUB2HXA"
target="_blank"
rel="noopener noreferrer"
>
{t('share_feedback_on_the_new_editor')}
</a>
</>
}
description={t('new_editor_info')}
checked={enabled}
onChange={handleToggle}
/>
@@ -7,7 +7,7 @@ export default function Setting({
description = undefined,
}: {
label: React.ReactNode
description: string | undefined
description: React.ReactNode | undefined
controlId: string
children: React.ReactNode
}) {
@@ -12,7 +12,7 @@ export default function ToggleSetting({
}: {
id: string
label: React.ReactNode
description: string
description: React.ReactNode
checked: boolean | undefined
onChange: (newValue: boolean) => void
disabled?: boolean
@@ -1,246 +0,0 @@
import { useIdeRedesignSwitcherContext } from '@/features/ide-react/context/ide-redesign-switcher-context'
import OLButton from '@/shared/components/ol/ol-button'
import {
OLModal,
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/shared/components/ol/ol-modal'
import { FC, useCallback, useEffect } from 'react'
import {
canUseNewEditor,
useIsNewEditorEnabled,
useIsNewEditorEnabledAsExistingUser,
} from '../../utils/new-editor-utils'
import Notification from '@/shared/components/notification'
import { useSwitchEnableNewEditorState } from '../../hooks/use-switch-enable-new-editor-state'
import { useTranslation } from 'react-i18next'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import { useSurveyUrl } from '../../hooks/use-survey-url'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useEditorContext } from '@/shared/context/editor-context'
const TUTORIAL_KEY = 'ide-redesign-beta-intro'
export const IdeRedesignIntroModal: FC = () => {
const { t } = useTranslation()
const { inactiveTutorials } = useEditorContext()
const { showPopup, tryShowingPopup, dismissTutorial } = useTutorial(
TUTORIAL_KEY,
{
name: TUTORIAL_KEY,
}
)
const hasAccess = useIsNewEditorEnabledAsExistingUser()
useEffect(() => {
if (!hasAccess) return
if (!inactiveTutorials.includes(TUTORIAL_KEY)) {
tryShowingPopup()
}
}, [tryShowingPopup, inactiveTutorials, hasAccess])
if (!hasAccess) {
return null
}
return (
<OLModal
show={showPopup}
onHide={dismissTutorial}
className="ide-redesign-switcher-modal"
>
<OLModalHeader>
<OLModalTitle>
{t('the_new_overleaf_editor_try_now_in_beta')}
</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>
{t(
'weve_redesigned_our_editor_to_make_it_easier_to_use_and_future_ready'
)}{' '}
{t('you_can_switch_back_to_the_old_editor_at_any_time')}
</p>
<SwitcherWhatsDifferent />
</OLModalBody>
<OLModalFooter>
<OLButton onClick={dismissTutorial} variant="secondary">
{t('close')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}
export const IdeRedesignSwitcherModal = () => {
const { t } = useTranslation()
const { showSwitcherModal, setShowSwitcherModal } =
useIdeRedesignSwitcherContext()
const onHide = useCallback(
() => setShowSwitcherModal(false),
[setShowSwitcherModal]
)
const { loading, error, setEditorRedesignStatus } =
useSwitchEnableNewEditorState()
const enabled = useIsNewEditorEnabled()
const hasAccess = canUseNewEditor()
if (!hasAccess) {
return null
}
const Content = enabled
? SwitcherModalContentEnabled
: SwitcherModalContentDisabled
return (
<OLModal
show={showSwitcherModal}
onHide={onHide}
className="ide-redesign-switcher-modal"
>
<OLModalHeader>
<OLModalTitle>
{enabled
? t('beta_program_the_new_overleaf_editor')
: t('the_new_overleaf_editor_try_now_in_beta')}
</OLModalTitle>
</OLModalHeader>
{error && <Notification type="error" content={error} isDismissible />}
<Content
setEditorRedesignStatus={setEditorRedesignStatus}
hide={onHide}
loading={loading}
/>
</OLModal>
)
}
type ModalContentProps = {
setEditorRedesignStatus: (enabled: boolean) => Promise<void>
hide: () => void
loading: boolean
}
const SwitcherModalContentEnabled: FC<ModalContentProps> = ({
setEditorRedesignStatus,
hide,
loading,
}) => {
const { t } = useTranslation()
const { sendEvent } = useEditorAnalytics()
const disable = useCallback(() => {
sendEvent('editor-redesign-toggle', {
action: 'disable',
location: 'modal',
})
setEditorRedesignStatus(false)
.then(hide)
.catch(() => {
// do nothing, we're already showing the error
})
}, [setEditorRedesignStatus, hide, sendEvent])
const surveyURL = useSurveyUrl()
return (
<>
<OLModalBody>
<p>
{t(
'weve_redesigned_our_editor_to_make_it_easier_to_use_and_future_ready'
)}
</p>
<SwitcherWhatsDifferent />
</OLModalBody>
<OLModalFooter>
<OLButton
onClick={disable}
variant="secondary"
className="me-auto"
disabled={loading}
>
{t('switch_to_old_editor')}
</OLButton>
<OLButton onClick={hide} variant="secondary">
{t('cancel')}
</OLButton>
<OLButton
href={surveyURL}
target="_blank"
rel="noopener noreferrer"
variant="primary"
>
{t('give_feedback')}
</OLButton>
</OLModalFooter>
</>
)
}
const SwitcherModalContentDisabled: FC<ModalContentProps> = ({
setEditorRedesignStatus,
hide,
loading,
}) => {
const { t } = useTranslation()
const { sendEvent } = useEditorAnalytics()
const enable = useCallback(() => {
sendEvent('editor-redesign-toggle', {
action: 'enable',
location: 'modal',
})
setEditorRedesignStatus(true)
.then(hide)
.catch(() => {
// do nothing, we're already showing the error
})
}, [setEditorRedesignStatus, hide, sendEvent])
return (
<>
<OLModalBody>
<p>
{t(
'weve_redesigned_our_editor_to_make_it_easier_to_use_and_future_ready'
)}
</p>
<SwitcherWhatsDifferent />
<LeavingNote />
</OLModalBody>
<OLModalFooter>
<OLButton onClick={hide} variant="secondary">
{t('cancel')}
</OLButton>
<OLButton onClick={enable} variant="primary" disabled={loading}>
{t('switch_to_new_editor')}
</OLButton>
</OLModalFooter>
</>
)
}
const SwitcherWhatsDifferent = () => {
const { t } = useTranslation()
return (
<div className="ide-redesign-switcher-modal-whats-new">
<h4>{t('whats_different')}</h4>
<ul>
<li>{t('new_look_and_feel')}</li>
<li>
{t('new_navigation_introducing_left_hand_side_rail_and_top_menus')}
</li>
<li>{t('new_look_and_placement_of_the_settings')}</li>
<li>{t('improved_dark_mode')}</li>
<li>{t('review_panel_and_error_logs_moved_to_the_left')}</li>
</ul>
</div>
)
}
const LeavingNote = () => {
const { t } = useTranslation()
return <p>{t('you_can_switch_back_to_the_old_editor_at_any_time')}</p>
}
@@ -1,42 +0,0 @@
import { useIdeRedesignSwitcherContext } from '@/features/ide-react/context/ide-redesign-switcher-context'
import OLButton from '@/shared/components/ol/ol-button'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { GiveFeedbackLink } from './give-feedback-link'
import { useIsNewEditorEnabledAsExistingUser } from '../../utils/new-editor-utils'
export const BetaActions = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const openEditorRedesignSwitcherModal = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
const showBetaActions = useIsNewEditorEnabledAsExistingUser()
if (!showBetaActions) {
return null
}
return (
<>
<div className="ide-redesign-toolbar-button-container">
<OLTooltip
id="tooltip-beta-button"
description={t('this_is_a_beta_release_for_the_new_overleaf_editor')}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<OLButton
size="sm"
variant="secondary"
className="ide-redesign-beta-button"
onClick={openEditorRedesignSwitcherModal}
>
{t('beta')}
</OLButton>
</OLTooltip>
</div>
<GiveFeedbackLink />
</>
)
}
@@ -1,20 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useSurveyUrl } from '../../hooks/use-survey-url'
export const GiveFeedbackLink = () => {
const { t } = useTranslation()
const surveyURL = useSurveyUrl()
return (
<div className="ide-redesign-toolbar-button-container">
<a
href={surveyURL}
rel="noopener noreferrer"
target="_blank"
className="ide-redesign-toolbar-labs-feedback-link"
>
{t('give_feedback')}
</a>
</div>
)
}
@@ -8,11 +8,7 @@ import { MenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown'
import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
import { useTranslation } from 'react-i18next'
import ChangeLayoutOptions from './change-layout-options'
import { MouseEventHandler, useCallback, useMemo, useState } from 'react'
import { useIdeRedesignSwitcherContext } from '@/features/ide-react/context/ide-redesign-switcher-context'
import { useSwitchEnableNewEditorState } from '../../hooks/use-switch-enable-new-editor-state'
import MaterialIcon from '@/shared/components/material-icon'
import OLSpinner from '@/shared/components/ol/ol-spinner'
import { useCallback, useMemo, useState } from 'react'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import CommandDropdown, {
@@ -24,27 +20,20 @@ import { useRailContext } from '../../contexts/rail-context'
import WordCountModal from '@/features/word-count-modal/components/word-count-modal'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useSurveyUrl } from '../../hooks/use-survey-url'
import getMeta from '@/utils/meta'
import EditorCloneProjectModalWrapper from '@/features/clone-project-modal/components/editor-clone-project-modal-wrapper'
import useOpenProject from '@/shared/hooks/use-open-project'
import { canUseNewEditorAsExistingUser } from '../../utils/new-editor-utils'
export const ToolbarMenuBar = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const openEditorRedesignSwitcherModal = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
const { setView, view } = useLayoutContext()
const { pdfUrl } = useCompileContext()
const wordCountEnabled = pdfUrl || isSplitTestEnabled('word-count-client')
const [showWordCountModal, setShowWordCountModal] = useState(false)
const [showCloneProjectModal, setShowCloneProjectModal] = useState(false)
const openProject = useOpenProject()
const showEditorSwitchMenuOption = canUseNewEditorAsExistingUser()
const anonymous = getMeta('ol-anonymous')
@@ -213,8 +202,6 @@ export const ToolbarMenuBar = () => {
setActiveModal('contact-us')
}, [setActiveModal])
const surveyURL = useSurveyUrl()
return (
<>
<MenuBar
@@ -285,24 +272,6 @@ export const ToolbarMenuBar = () => {
title={t('contact_us')}
onClick={openContactUsModal}
/>
{showEditorSwitchMenuOption && (
<>
<MenuBarOption
eventKey="give_feedback"
title={t('give_feedback')}
href={surveyURL}
target="_blank"
rel="noopener noreferrer"
/>
<DropdownDivider />
<SwitchToOldEditorMenuBarOption />
<MenuBarOption
eventKey="whats_new"
title="What's new?"
onClick={openEditorRedesignSwitcherModal}
/>
</>
)}
</MenuBarDropdown>
</MenuBar>
<WordCountModal
@@ -317,37 +286,3 @@ export const ToolbarMenuBar = () => {
</>
)
}
const SwitchToOldEditorMenuBarOption = () => {
const { loading, error, setEditorRedesignStatus } =
useSwitchEnableNewEditorState()
const { sendEvent } = useEditorAnalytics()
const disable: MouseEventHandler = useCallback(
event => {
// Don't close the dropdown
event.stopPropagation()
sendEvent('editor-redesign-toggle', {
action: 'disable',
location: 'menu-bar',
})
setEditorRedesignStatus(false)
},
[setEditorRedesignStatus, sendEvent]
)
let icon = null
if (loading) {
icon = <OLSpinner size="sm" />
} else if (error) {
icon = <MaterialIcon type="error" title={error} className="text-danger" />
}
return (
<MenuBarOption
eventKey="switch_to_old_editor"
title="Switch to old editor"
onClick={disable}
disabled={loading}
trailingIcon={icon}
/>
)
}
@@ -15,7 +15,6 @@ import importOverleafModules from '../../../../../macros/import-overleaf-module.
import UpgradeButton from './upgrade-button'
import getMeta from '@/utils/meta'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { BetaActions } from './beta-actions'
const [publishModalModules] = importOverleafModules('publishModal')
const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
@@ -54,7 +53,6 @@ export const Toolbar = () => {
</div>
<ToolbarProjectTitle />
<div className="ide-redesign-toolbar-actions">
<BetaActions />
<OnlineUsers />
{!isRestrictedTokenMember && <ShowHistoryButton />}
<ChangeLayoutButton />
@@ -44,6 +44,10 @@ export default function TooltipPromotion({
hideUntilReload()
}, [hideUntilReload])
const onDismiss = useCallback(() => {
dismissTutorial()
}, [dismissTutorial])
if (!target || !isInSplitTestIfNeeded) {
return null
}
@@ -60,13 +64,13 @@ export default function TooltipPromotion({
{header && (
<Popover.Header>
{header}
<Close variant="dark" onDismiss={dismissTutorial} />
<Close variant="dark" onDismiss={onDismiss} />
</Popover.Header>
)}
<Popover.Body className={classNames(className)}>
{content}
{!header && <Close variant="dark" onDismiss={dismissTutorial} />}
{!header && <Close variant="dark" onDismiss={onDismiss} />}
</Popover.Body>
</Popover>
</Overlay>
@@ -0,0 +1,114 @@
import { useLayoutContext } from '@/shared/context/layout-context'
import {
createContext,
FC,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
export type NewEditorTourStage = 'rail' | 'logs' | 'theme' | 'switch-back'
const NewEditorTourContext = createContext<
| {
stage: NewEditorTourStage
stageNumber: number
totalStages: number
shouldShowTourStage: (tourStage: NewEditorTourStage) => boolean
startTour: () => void
goToNextStage: () => void
finishTour: () => void
dismissTour: () => void
}
| undefined
>(undefined)
const STAGES: NewEditorTourStage[] = ['rail', 'logs', 'theme', 'switch-back']
const EDITOR_ONLY_STAGES: NewEditorTourStage[] = [
'rail',
'theme',
'switch-back',
]
export const NewEditorTourProvider: FC<React.PropsWithChildren> = ({
children,
}) => {
const [stage, setStage] = useState<NewEditorTourStage>('rail')
const [showTour, setShowTour] = useState(false)
const { view, pdfLayout } = useLayoutContext()
const pdfIsOpen = pdfLayout === 'sideBySide' || view === 'pdf'
const stagesToShow = useMemo(
() => (pdfIsOpen ? STAGES : EDITOR_ONLY_STAGES),
[pdfIsOpen]
)
const startTour = useCallback(() => {
setShowTour(true)
}, [])
const stageNumber = useMemo(
() => stagesToShow.indexOf(stage) + 1,
[stage, stagesToShow]
)
const totalStages = stagesToShow.length
const goToNextStage = useCallback(() => {
setStage(stagesToShow[stageNumber])
}, [stageNumber, stagesToShow])
const dismissTour = useCallback(() => {
setShowTour(false)
}, [])
const finishTour = useCallback(() => {
setShowTour(false)
}, [])
const shouldShowTourStage = useCallback(
(tourStage: NewEditorTourStage) => {
return showTour && stage === tourStage
},
[showTour, stage]
)
const value = useMemo(
() => ({
stage,
stageNumber,
totalStages,
shouldShowTourStage,
startTour,
goToNextStage,
finishTour,
dismissTour,
}),
[
stage,
stageNumber,
totalStages,
shouldShowTourStage,
startTour,
goToNextStage,
finishTour,
dismissTour,
]
)
return (
<NewEditorTourContext.Provider value={value}>
{children}
</NewEditorTourContext.Provider>
)
}
export const useNewEditorTourContext = () => {
const context = useContext(NewEditorTourContext)
if (!context) {
throw new Error(
'useNewEditorTourContext is only available inside RailProvider'
)
}
return context
}
@@ -25,7 +25,6 @@ import FontFamilySetting from '../components/settings/appearance-settings/font-f
import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import NewEditorSetting from '../components/settings/editor-settings/new-editor-setting'
import { canUseNewEditorAsNewUser } from '../utils/new-editor-utils'
const [referenceSearchSettingModule] = importOverleafModules(
'referenceSearchSetting'
@@ -77,7 +76,6 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
children,
}) => {
const { t } = useTranslation()
const showEditorSwitch = canUseNewEditorAsNewUser()
// TODO ide-redesign-cleanup: Rename this field and move it directly into this context
const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
@@ -215,7 +213,6 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
{
key: 'newEditor',
component: <NewEditorSetting />,
hidden: !showEditorSwitch,
},
],
},
@@ -234,7 +231,7 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
href: '/user/subscription',
},
],
[t, showEditorSwitch]
[t]
)
const settingToTabMap = useMemo(() => {
@@ -1,10 +0,0 @@
import { useSplitTest } from '@/shared/context/split-test-context'
export const useSurveyUrl = () => {
const splitTestConfig = useSplitTest('editor-redesign')
return (
splitTestConfig.info?.badgeInfo?.url ||
'https://forms.gle/NGkALNUiMbanjp3Q7'
)
}
@@ -11,4 +11,5 @@ export type RailElement = {
hide?: boolean | (() => boolean)
disabled?: boolean
mountOnFirstLoad?: boolean
ref?: React.RefObject<HTMLAnchorElement>
}
@@ -1,4 +1,4 @@
import { memo, useCallback } from 'react'
import { forwardRef, memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
@@ -7,7 +7,7 @@ import OLTooltip from '@/shared/components/ol/ol-tooltip'
import OLButton from '@/shared/components/ol/ol-button'
import OLBadge from '@/shared/components/ol/ol-badge'
function PdfHybridLogsButton() {
const PdfHybridLogsButton = forwardRef<HTMLButtonElement>((_, ref) => {
const { error, logEntries, toggleLogs, showLogs, stoppedOnFirstError } =
useCompileContext()
@@ -32,6 +32,7 @@ function PdfHybridLogsButton() {
overlayProps={{ placement: 'bottom' }}
>
<OLButton
ref={ref}
variant="link"
disabled={Boolean(error || stoppedOnFirstError)}
active={showLogs}
@@ -50,6 +51,8 @@ function PdfHybridLogsButton() {
</OLButton>
</OLTooltip>
)
}
})
PdfHybridLogsButton.displayName = 'PdfHybridLogsButton'
export default memo(PdfHybridLogsButton)
@@ -19,7 +19,7 @@ const useTutorial = (
action = 'complete',
...rest
}: {
event: 'promo-click' | 'promo-dismiss'
event: string
action: 'complete' | 'postpone'
} & Record<string, any>) => {
eventTracking.sendMB(event, { ...eventData, ...rest })
@@ -34,12 +34,15 @@ const useTutorial = (
[deactivateTutorial, eventData, tutorialKey]
)
const dismissTutorial = useCallback(async () => {
await completeTutorial({
event: 'promo-dismiss',
action: 'complete',
})
}, [completeTutorial])
const dismissTutorial = useCallback(
async (eventName: string = 'promo-dismiss') => {
await completeTutorial({
event: eventName,
action: 'complete',
})
},
[completeTutorial]
)
const maybeLater = useCallback(async () => {
await completeTutorial({
@@ -7,7 +7,6 @@
@import 'sidebar-v2-dash-pane';
@import 'editor/ide';
@import 'editor/ide-redesign';
@import 'editor/ide-redesign-switcher-modal';
@import 'editor/rail';
@import 'editor/settings';
@import 'editor/toolbar';
@@ -36,6 +35,8 @@
@import 'editor/math-preview';
@import 'editor/references-search';
@import 'editor/editor-survey';
@import 'editor/editor-tour-tooltip';
@import 'editor/new-editor-promo-modal';
@import 'error-pages';
@import 'website-redesign';
@import 'group-settings';
@@ -0,0 +1,43 @@
.editor-tour-tooltip {
--bs-popover-bg: var(--bg-light-primary);
--bs-popover-header-bg: var(--bg-light-primary);
--bs-popover-body-color: var(--content-primary);
--bs-popover-header-color: var(--content-primary);
--editor-tour-tooltip-link-color: var(--link-ui);
--editor-tour-tooltip-link-hover-color: var(--link-ui-hover);
--editor-tour-tooltip-link-visited-color: var(--link-ui-visited);
a {
color: var(--editor-tour-tooltip-link-color);
&:visited {
color: var(--editor-tour-tooltip-link-hover-color);
}
&:hover {
color: var(--editor-tour-tooltip-link-visited-color);
}
}
}
@include theme('light') {
.editor-tour-tooltip {
--bs-popover-bg: var(--bg-dark-primary);
--bs-popover-header-bg: var(--bg-dark-primary);
--bs-popover-body-color: var(--content-primary-dark);
--bs-popover-header-color: var(--content-primary-dark);
--editor-tour-tooltip-link-color: var(--link-ui-dark);
--editor-tour-tooltip-link-hover-color: var(--link-ui-hover-dark);
--editor-tour-tooltip-link-visited-color: var(--link-ui-visited-dark);
}
}
.editor-tour-tooltip-footer {
display: flex;
justify-content: space-between;
align-items: center;
.btn-link {
color: var(--bs-popover-body-color);
}
}
@@ -1,32 +0,0 @@
.ide-redesign-switcher-modal .modal-content {
color: var(--content-primary);
font-size: var(--font-size-03);
line-height: var(--line-height-03);
p {
margin-bottom: 0;
}
.ide-redesign-switcher-modal-whats-new {
background-color: var(--bg-light-secondary);
border: 1px solid var(--border-divider);
padding: var(--spacing-05);
margin: var(--spacing-05) 0;
hr {
margin: var(--spacing-04) 0;
}
h4 {
margin-top: 0;
}
ul {
margin-bottom: 0;
li:not(:last-child) {
margin-bottom: var(--spacing-04);
}
}
}
}
@@ -0,0 +1,11 @@
.new-editor-promo-modal-body,
.new-editor-intro-modal-body {
display: flex;
flex-direction: column;
gap: var(--spacing-04);
video {
border-radius: var(--border-radius-base);
width: 100%;
}
}
@@ -98,18 +98,3 @@
justify-content: flex-end;
margin-left: var(--spacing-06);
}
.ide-setting-new-editor {
display: flex;
gap: var(--spacing-04);
}
.ide-setting-beta-tag {
font-size: var(--font-size-01);
line-height: var(--line-height-01);
color: var(--green-60);
background: var(--bg-accent-03);
border: 1px solid var(--green-50);
border-radius: var(--border-radius-full);
padding: var(--spacing-01) var(--spacing-03);
}
@@ -540,3 +540,9 @@
font-size: var(--font-size-01);
margin-right: var(--spacing-04);
}
.try-new-editor-button {
.button-content {
gap: var(--spacing-02);
}
}
+20 -18
View File
@@ -233,6 +233,7 @@
"basic": "Basic",
"basic_compile_time": "Basic compile time",
"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",
"beta": "Beta",
"beta_feature_badge": "Beta feature badge",
@@ -242,7 +243,6 @@
"beta_program_not_participating": "You are not enrolled in the beta program",
"beta_program_opt_in_action": "Opt-in to beta program",
"beta_program_opt_out_action": "Opt-out of beta program",
"beta_program_the_new_overleaf_editor": "Beta program: the new Overleaf editor",
"bibliographies": "Bibliographies",
"billed_annually_at": "Billed annually at <0>__price__</0> <1>(includes plan and any add-ons)</1>",
"billed_monthly_at": "Billed monthly at <0>__price__</0> <1>(includes plan and any add-ons)</1>",
@@ -307,6 +307,7 @@
"certificate": "Certificate",
"change": "Change",
"change_currency": "Change currency",
"change_how_you_see_the_editor": "Change how you see the editor using the updated <strong>Appearance</strong> settings.",
"change_language": "Change language",
"change_or_cancel-cancel": "cancel",
"change_or_cancel-change": "Change",
@@ -749,6 +750,7 @@
"expires_on": "Expires: __date__",
"expiry": "Expiry Date",
"explore_all_plans": "Explore all plans",
"explore_what_s_new": "Explore whats new",
"export_csv": "Export CSV",
"export_project_to_github": "Export Project to GitHub",
"failed_to_send_group_invite_to_email": "Failed to send Group invite to <0>__email__</0>. Please try again later.",
@@ -792,11 +794,13 @@
"filter_projects": "Filter projects",
"filters": "Filters",
"find": "Find",
"find_and_fix_errors_faster": "Find and fix errors faster",
"find_out_more": "Find out More",
"find_out_more_about_institution_login": "Find out more about institutional login",
"find_out_more_about_the_file_outline": "Find out more about the file outline",
"find_out_more_nt": "Find out more.",
"finding_a_fix": "Finding a fix",
"finish": "Finish",
"first_name": "First name",
"fit_to_height": "Fit to height",
"fit_to_width": "Fit to width",
@@ -1072,7 +1076,6 @@
"imported_from_zotero_at_date": "Imported from Zotero at __formattedDate__ __relativeDate__",
"importing": "Importing",
"importing_and_merging_changes_in_github": "Importing and merging changes in GitHub",
"improved_dark_mode": "Improved dark mode",
"in_order_to_have_a_secure_account_make_sure_your_password": "To help keep your account secure, make sure your new password:",
"in_order_to_match_institutional_metadata_2": "In order to match your institutional metadata, weve linked your account using <0>__email__</0>.",
"in_order_to_match_institutional_metadata_associated": "In order to match your institutional metadata, your account is associated with the email <b>__email__</b>.",
@@ -1123,6 +1126,7 @@
"integrations": "Integrations",
"integrations_like_github": "Integrations like GitHub Sync",
"interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <0>__price__</0> Personal plan?",
"introducing_overleafs_new_look": "Introducing __appName__s new look",
"invalid_certificate": "Invalid certificate. Please check the certificate and try again.",
"invalid_confirmation_code": "That didnt work. Please check the code and try again.",
"invalid_email": "An email address is invalid",
@@ -1438,15 +1442,11 @@
"new_compile_domain_notice": "Weve 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_compiles_in_this_project_will_automatically_use_the_newest_version": "New compiles in this project will automatically use the newest version. <0>Learn how to change compiler settings</0>",
"new_create_tables_and_equations": "NEW! <sparkle/> Create tables and equations in seconds",
"new_editor": "New editor",
"new_editor_experience": "New editor experience",
"new_editor_info": "Our new editor is currently in beta. Disabling this option will change your experience to the old Overleaf editor.",
"new_editor_look": "New editor look",
"new_error_logs_make_it_easier_to_find_whats_wrong": "New error logs make it easier to find whats wrong and fix your document, so you can get compiling again.",
"new_file": "New file",
"new_folder": "New folder",
"new_look_and_feel": "New look and feel",
"new_look_and_placement_of_the_settings": "New look and placement of the settings",
"new_name": "New name",
"new_navigation_introducing_left_hand_side_rail_and_top_menus": "New navigation - introducing left-hand side rail and top menus",
"new_password": "New password",
"new_project": "New project",
"new_snippet_project": "Untitled",
@@ -1507,6 +1507,7 @@
"not_managed": "Not managed",
"not_now": "Not now",
"not_registered": "Not registered",
"not_sure_about_switching_yet": "Not sure about switching yet?",
"note_features_under_development": "<0>Please note</0> that features in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a premium plan</0>",
"notification": "Notification",
"notification_features_upgraded_by_affiliation": "Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to all of Overleafs Professional features.",
@@ -1588,6 +1589,7 @@
"overleaf_plans_and_pricing": "overleaf plans and pricing",
"overleaf_template_gallery": "overleaf template gallery",
"overleafs_functionality_meets_my_needs": "Overleafs functionality meets my needs.",
"overleafs_new_look_is_here": "__appName__s new look is here",
"overview": "Overview",
"overwrite": "Overwrite",
"overwriting_the_original_folder": "Overwriting the original folder will delete it and all the files it contains.",
@@ -1809,6 +1811,7 @@
"read_lines_from_path": "Read lines from __path__",
"read_more": "Read more",
"read_more_about_managed_users": "Read more about managed users",
"read_more_about_the_new_editor": "<0>Read more about the new editor design</0>, or temporarily switch back to the old editor using the <1>Appearance</1> settings.",
"read_only_dropbox_sync_message": "As a read-only viewer you can sync the current project version to Dropbox, but changes made in Dropbox will <0>not</0> sync back to Overleaf.",
"read_only_token": "Read-Only Token",
"read_write_token": "Read-Write Token",
@@ -1951,7 +1954,6 @@
"revert_pending_plan_change": "Revert scheduled plan change",
"review": "Review",
"review_panel": "Review panel",
"review_panel_and_error_logs_moved_to_the_left": "Review panel and error logs moved to the left",
"reviewer": "Reviewer",
"reviewer_dropbox_sync_message": "As a reviewer you can sync the current project version to Dropbox, but changes made in Dropbox will <0>not</0> sync back to Overleaf.",
"reviewing": "Reviewing",
@@ -2085,6 +2087,7 @@
"setup_another_account_under_a_personal_email_address": "Set up another Overleaf account under a personal email address.",
"share": "Share",
"share_feedback": "Share feedback",
"share_feedback_on_the_new_editor": "Share feedback on the new editor look.",
"share_project": "Share Project",
"shared_with_you": "Shared with you",
"sharelatex_beta_program": "__appName__ beta program",
@@ -2114,6 +2117,7 @@
"sign_up_for_free": "Sign up for free",
"sign_up_for_free_account": "Sign up for a free account and receive regular updates",
"simple_search_mode": "Simple search",
"simplified_working_starts_here": "Simplified working starts here",
"single_sign_on_sso": "Single Sign-On (SSO)",
"site_description": "An online LaTeX editor thats easy to use. No installation, real-time collaboration, version control, hundreds of LaTeX templates, and more.",
"site_wide_option_available": "Site-wide option available",
@@ -2267,10 +2271,10 @@
"sure_you_want_to_delete": "Are you sure you want to permanently delete the following files?",
"sure_you_want_to_leave_group": "Are you sure you want to leave this group?",
"sv": "Swedish",
"switch_between_dark_and_light_mode": "Switch between dark and light mode",
"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": "Switch to new editor",
"switch_to_old_editor": "Switch to old editor",
"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",
@@ -2346,8 +2350,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_overleaf_editor_try_now_in_beta": "The new Overleaf editor — try now in beta",
"the_next_payment_will_be_collected_on": "The next payment will be collected on <strong>__date__</strong>.",
"the_new_overleaf_editor_info": "__appName__s new look is here. Disabling this option will switch you back to the old editor design.",
"the_original_text_has_changed": "The original text has changed, so this suggestion cant be applied",
"the_overleaf_color_scheme": "The __appName__ color scheme",
"the_primary_file_for_compiling_your_project": "The primary file for compiling your project",
@@ -2385,7 +2388,6 @@
"this_experiment_isnt_accepting_new_participants": "This experiment isnt accepting new participants.",
"this_field_is_required": "This field is required",
"this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.",
"this_is_a_beta_release_for_the_new_overleaf_editor": "This is a beta release for the new Overleaf editor. You can switch back to the old editor at any time.",
"this_is_a_new_feature": "This is a new feature",
"this_is_the_file_that_references_pulled_from_your_reference_manager_will_be_added_to": "This is the file that references pulled from your reference manager will be added to.",
"this_is_your_template": "This is your template from your project",
@@ -2520,10 +2522,12 @@
"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 out the new design now (you can switch back at any time), or <0>read more about the changes were making</0>.",
"try_premium_for_free": "Try Premium for free",
"try_recompile_project_or_troubleshoot": "Please try recompiling the project from scratch, and if that doesnt help, follow our <0>troubleshooting guide</0>.",
"try_relinking_provider": "It looks like you need to re-link your __provider__ account.",
"try_the_new_editor": "Try the new editor",
"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",
"turn_off": "Turn off",
"turn_off_link_sharing": "Turn off link sharing",
@@ -2701,14 +2705,13 @@
"well_be_here_when_youre_ready": "Well be here when youre ready to dive back in! 🦆",
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "Were making some <0>changes to project sharing</0>. This means, as someone with edit access, your name and email address will be visible to the project owner and other editors.",
"were_performing_maintenance": "Were performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.",
"weve_redesigned_our_editor_to_make_it_easier_to_use_and_future_ready": "Weve redesigned our editor to make it easier to use and future ready. Its now in beta, so try it out and give us your feedback.",
"weve_made_it_easier_to_find_and_use_the_tools_you_need_today": "The new editor design makes it easier to find and use the tools you need today, while making space for the new features youll love tomorrow.",
"what_did_you_find_most_helpful": "What did you find most helpful?",
"what_do_you_need": "What do you need?",
"what_do_you_need_help_with": "What do you need help with?",
"what_does_this_mean_for_you": "This means:",
"what_happens_when_sso_is_enabled": "What happens when SSO is enabled?",
"what_should_we_call_you": "What should we call you?",
"whats_different": "Whats different?",
"when_you_join_labs": "When you join Labs, you can choose which experiments you want to be part of. Once youve done that, you can use Overleaf as normal, but youll see any labs features marked with this badge:",
"when_you_tick_the_include_caption_box": "When you tick the box “Include caption” the image will be inserted into your document with a placeholder caption. To edit it, you simply select the placeholder text and type to replace it with your own.",
"why_latex": "Why LaTeX?",
@@ -2766,7 +2769,6 @@
"you_can_select_or_invite_collaborator": "You can select or invite __count__ collaborator on your current plan. Upgrade to add more editors or reviewers.",
"you_can_select_or_invite_collaborator_plural": "You can select or invite __count__ collaborators on your current plan. Upgrade to add more editors or reviewers.",
"you_can_still_use_your_premium_features": "You can still use your premium features until the pause becomes active.",
"you_can_switch_back_to_the_old_editor_at_any_time": "You can switch back to the old editor at any time.",
"you_cant_add_or_change_password_due_to_sso": "You cant add or change your password because your group or organization uses <0>single sign-on (SSO)</0>.",
"you_cant_join_this_group_subscription": "You cant join this group subscription",
"you_cant_reset_password_due_to_sso": "You cant reset your password because your group or organization uses SSO. <0>Log in with SSO</0>.",