Merge pull request #24137 from overleaf/mj-ide-permissions-utils

[web] Add switcher for editor redesign

GitOrigin-RevId: 806a1f567027df53f879b564a50aaae9166c8480
This commit is contained in:
David
2025-03-17 15:08:07 +00:00
committed by Copybot
parent 18b00aa02b
commit 062b2d57f8
32 changed files with 554 additions and 19 deletions
@@ -847,6 +847,7 @@ const _ProjectController = {
overallTheme: user.ace.overallTheme,
mathPreview: user.ace.mathPreview,
referencesSearchMode: user.ace.referencesSearchMode,
enableNewEditor: user.ace.enableNewEditor ?? true,
},
privilegeLevel,
anonymous,
@@ -392,6 +392,9 @@ async function updateUserSettings(req, res, next) {
req.body.referencesSearchMode === 'simple' ? 'simple' : 'advanced'
user.ace.referencesSearchMode = mode
}
if (req.body.enableNewEditor != null) {
user.ace.enableNewEditor = Boolean(req.body.enableNewEditor)
}
await user.save()
const newEmail = req.body.email?.trim().toLowerCase()
+1
View File
@@ -98,6 +98,7 @@ const UserSchema = new Schema(
lineHeight: { type: String },
mathPreview: { type: Boolean, default: true },
referencesSearchMode: { type: String, default: 'advanced' }, // 'advanced' or 'simple'
enableNewEditor: { type: Boolean },
},
features: {
collaborators: {
@@ -352,6 +352,7 @@
"customize_your_group_subscription": "",
"customizing_figures": "",
"customizing_tables": "",
"dark_mode": "",
"date_and_owner": "",
"dealing_with_errors": "",
"delete": "",
@@ -694,6 +695,7 @@
"help_articles_matching": "",
"help_improve_overleaf_fill_out_this_survey": "",
"help_improve_screen_reader_fill_out_this_survey": "",
"help_shape_the_future_of_overleaf": "",
"hide": "",
"hide_configuration": "",
"hide_deleted_user": "",
@@ -852,6 +854,7 @@
"knowledge_base": "",
"labels_help_you_to_easily_reference_your_figures": "",
"labels_help_you_to_reference_your_tables": "",
"labs": "",
"language": "",
"language_suggestions_for_texts_in_any_language": "",
"large_or_high-resolution_images_taking_too_long": "",
@@ -1395,6 +1398,7 @@
"reverse_x_sort_order": "",
"revert_pending_plan_change": "",
"review": "",
"review_panel_comments_and_track_changes": "",
"review_your_peers_work": "",
"reviewer": "",
"reviewing": "",
@@ -1503,6 +1507,7 @@
"set_up_single_sign_on": "",
"set_up_sso": "",
"settings": "",
"settings_for_git_github_and_dropbox_integrations": "",
"setup_another_account_under_a_personal_email_address": "",
"share": "",
"share_project": "",
@@ -1644,6 +1649,8 @@
"switch_back_to_monthly_pay_20_more": "",
"switch_plan": "",
"switch_to_editor": "",
"switch_to_new_editor": "",
"switch_to_old_editor": "",
"switch_to_pdf": "",
"switch_to_standard_plan": "",
"symbol_palette": "",
@@ -1682,6 +1689,7 @@
"texgpt_for_help_writing_latex": "",
"thank_you_exclamation": "",
"thank_you_for_your_feedback": "",
"thanks_for_being_part_of_this_labs_experiment_your_feedback_will_help_us_make_the_new_editor_the_best_yet": "",
"thanks_for_confirming_your_email_address": "",
"thanks_for_getting_in_touch": "",
"thanks_for_subscribing": "",
@@ -1695,6 +1703,7 @@
"the_following_files_and_folders_already_exist_in_this_project": "",
"the_following_folder_already_exists_in_this_project": "",
"the_following_folder_already_exists_in_this_project_plural": "",
"the_new_overleaf_editor": "",
"the_next_payment_will_be_collected_on": "",
"the_original_text_has_changed": "",
"the_target_folder_could_not_be_found": "",
@@ -1713,6 +1722,7 @@
"this_experiment_isnt_accepting_new_participants": "",
"this_field_is_required": "",
"this_grants_access_to_features_2": "",
"this_is_a_labs_experiment_for_the_new_overleaf_editor_some_features_are_still_in_progress": "",
"this_is_a_new_feature": "",
"this_is_the_file_that_references_pulled_from_your_reference_manager_will_be_added_to": "",
"this_project_already_has_maximum_editors": "",
@@ -1843,6 +1853,7 @@
"try_premium_for_free": "",
"try_recompile_project_or_troubleshoot": "",
"try_relinking_provider": "",
"try_the_new_editor": "",
"try_to_compile_despite_errors": "",
"turn_off": "",
"turn_off_link_sharing": "",
@@ -1992,6 +2003,7 @@
"well_be_here_when_youre_ready": "",
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
"were_performing_maintenance": "",
"were_redesigning_our_editor_to_make_it_easier_to_use": "",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "",
"what_do_you_need_help_with": "",
@@ -1999,6 +2011,8 @@
"what_does_this_mean_for_you": "",
"what_happens_when_sso_is_enabled": "",
"what_should_we_call_you": "",
"whats_new": "",
"whats_next": "",
"when_you_tick_the_include_caption_box": "",
"why_latex": "",
"why_might_this_happen": "",
@@ -2039,6 +2053,7 @@
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
"you_can_also_choose_to_view_anonymously_or_leave_the_project": "",
"you_can_buy_this_plan_but_not_as_a_trial": "",
"you_can_leave_the_experiment_from_your_account_settings_at_any_time": "",
"you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "",
"you_can_now_enable_sso": "",
"you_can_now_log_in_sso": "",
@@ -2109,6 +2124,7 @@
"youre_about_to_enable_single_sign_on_sso_only": "",
"youre_adding_x_users_to_your_plan_giving_you_a_total_of_y_users": "",
"youre_already_setup_for_sso": "",
"youre_helping_us_shape_the_future_of_overleaf": "",
"youre_joining": "",
"youre_on_free_trial_which_ends_on": "",
"youre_signed_in_as_logout": "",
@@ -9,6 +9,7 @@ export default /** @type {const} */ ([
'code',
'create_new_folder',
'description',
'experiment',
'forum',
'help',
'image',
@@ -16,6 +16,8 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
import BackToEditorButton from './back-to-editor-button'
import getMeta from '@/utils/meta'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { canUseNewEditor } from '@/features/ide-redesign/utils/new-editor-utils'
import TryNewEditorButton from '../try-new-editor-button'
const [publishModalModules] = importOverleafModules('publishModal')
const PublishButton = publishModalModules?.import.default
@@ -87,6 +89,8 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
/>
<div className="toolbar-right">
{canUseNewEditor() && <TryNewEditorButton />}
<OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
{historyIsOpen ? (
@@ -0,0 +1,28 @@
import { useCallback } from 'react'
import OLButton from '../ui/components/ol/ol-button'
import { useIdeRedesignSwitcherContext } from '../ide-react/context/ide-redesign-switcher-context'
import MaterialIcon from '@/shared/components/material-icon'
import { useTranslation } from 'react-i18next'
const TryNewEditorButton = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const onClick = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
return (
<div className="d-flex align-items-center">
<OLButton
className="toolbar-experiment-button"
onClick={onClick}
size="sm"
leadingIcon={<MaterialIcon type="experiment" unfilled />}
variant="info"
>
{t('try_the_new_editor')}
</OLButton>
</div>
)
}
export default TryNewEditorButton
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
function FileTreeFolderIcons({
expanded,
@@ -10,8 +10,7 @@ function FileTreeFolderIcons({
onExpandCollapseClick: () => void
}) {
const { t } = useTranslation()
const newEditor = useFeatureFlag('editor-redesign')
const newEditor = useIsNewEditorEnabled()
if (newEditor) {
return (
@@ -4,7 +4,7 @@ import iconTypeFromName, {
} from '../util/icon-type-from-name'
import classnames from 'classnames'
import MaterialIcon from '@/shared/components/material-icon'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
function FileTreeIcon({
isLinkedFile,
@@ -19,7 +19,7 @@ function FileTreeIcon({
'linked-file-icon': isLinkedFile,
})
const newEditor = useFeatureFlag('editor-redesign')
const newEditor = useIsNewEditorEnabled()
if (newEditor) {
return (
@@ -19,7 +19,7 @@ import FileTreeInner from './file-tree-inner'
import { useDragLayer } from 'react-dnd'
import classnames from 'classnames'
import { pathInFolder } from '@/features/file-tree/util/path'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
const FileTreeRoot = React.memo<{
onSelect: () => void
@@ -43,7 +43,7 @@ const FileTreeRoot = React.memo<{
const { _id: projectId } = useProjectContext()
const { fileTreeData } = useFileTreeData()
const isReady = Boolean(projectId && fileTreeData)
const newEditor = useFeatureFlag('editor-redesign')
const newEditor = useIsNewEditorEnabled()
useEffect(() => {
if (fileTreeContainer) {
@@ -10,7 +10,7 @@ import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-e
import { Modals } from '@/features/ide-react/components/modals/modals'
import { GlobalAlertsProvider } from '@/features/ide-react/context/global-alerts-context'
import { GlobalToasts } from '../global-toasts'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
const MainLayoutNew = lazy(
() => import('@/features/ide-redesign/components/main-layout')
@@ -26,7 +26,7 @@ export default function IdePage() {
useRegisterUserActivity() // record activity and ensure connection when user is active
useHasLintingError() // pass editor:lint hasLintingError to the compiler
const newEditor = useFeatureFlag('editor-redesign')
const newEditor = useIsNewEditorEnabled()
return (
<GlobalAlertsProvider>
@@ -2,6 +2,7 @@ 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 } from '@/features/ide-redesign/components/switcher-modal/modal'
export const Modals = memo(() => {
return (
@@ -9,6 +10,7 @@ export const Modals = memo(() => {
<ForceDisconnected />
<UnsavedDocs />
<SystemMessages />
<IdeRedesignSwitcherModal />
</>
)
})
@@ -0,0 +1,39 @@
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 = ({ 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
}
@@ -24,6 +24,7 @@ import { SnapshotProvider } from '@/features/ide-react/context/snapshot-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { UserProvider } from '@/shared/context/user-context'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
import { IdeRedesignSwitcherProvider } from './ide-redesign-switcher-context'
export const ReactContextRoot: FC<{ providers?: Record<string, FC> }> = ({
children,
@@ -55,6 +56,7 @@ export const ReactContextRoot: FC<{ providers?: Record<string, FC> }> = ({
SplitTestProvider,
UserProvider,
UserSettingsProvider,
IdeRedesignSwitcherProvider,
...providers,
}
@@ -84,7 +86,9 @@ export const ReactContextRoot: FC<{ providers?: Record<string, FC> }> = ({
<Providers.MetadataProvider>
<Providers.OutlineProvider>
<Providers.RailProvider>
{children}
<Providers.IdeRedesignSwitcherProvider>
{children}
</Providers.IdeRedesignSwitcherProvider>
</Providers.RailProvider>
</Providers.OutlineProvider>
</Providers.MetadataProvider>
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useRailContext } from '../../contexts/rail-context'
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useIsNewEditorEnabled } from '../../utils/new-editor-utils'
function PdfErrorState() {
const { loadingError } = usePdfPreviewContext()
@@ -12,7 +12,7 @@ function PdfErrorState() {
const { showLogs } = useCompileContext()
const { t } = useTranslation()
const { setSelectedTab: setSelectedRailTab } = useRailContext()
const newEditor = useFeatureFlag('editor-redesign')
const newEditor = useIsNewEditorEnabled()
if (!newEditor || (!loadingError && !showLogs)) {
return null
@@ -0,0 +1,178 @@
import { useIdeRedesignSwitcherContext } from '@/features/ide-react/context/ide-redesign-switcher-context'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import { FC, useCallback } from 'react'
import {
canUseNewEditor,
useIsNewEditorEnabled,
} from '../../utils/new-editor-utils'
import Notification from '@/shared/components/notification'
import { useSwitchEnableNewEditorState } from '../../hooks/use-switch-enable-new-editor-state'
import { Trans, useTranslation } from 'react-i18next'
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 closeButton>
<OLModalTitle>{t('the_new_overleaf_editor')}</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 disable = useCallback(() => {
setEditorRedesignStatus(false)
.then(hide)
.catch(() => {
// do nothing, we're already showing the error
})
}, [setEditorRedesignStatus, hide])
return (
<>
<OLModalBody>
<h3>{t('youre_helping_us_shape_the_future_of_overleaf')}</h3>
<p>
{t(
'thanks_for_being_part_of_this_labs_experiment_your_feedback_will_help_us_make_the_new_editor_the_best_yet'
)}
</p>
<SwitcherWhatsNew />
<LeavingNote />
</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="https://forms.gle/soyVStc5qDx9na1Z6"
target="_blank"
rel="noopener noreferrer"
variant="primary"
>
{t('give_feedback')}
</OLButton>
</OLModalFooter>
</>
)
}
const SwitcherModalContentDisabled: FC<ModalContentProps> = ({
setEditorRedesignStatus,
hide,
loading,
}) => {
const { t } = useTranslation()
const enable = useCallback(() => {
setEditorRedesignStatus(true)
.then(hide)
.catch(() => {
// do nothing, we're already showing the error
})
}, [setEditorRedesignStatus, hide])
return (
<>
<OLModalBody>
<h3>{t('help_shape_the_future_of_overleaf')}</h3>
<p>{t('were_redesigning_our_editor_to_make_it_easier_to_use')}</p>
<SwitcherWhatsNew />
<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 SwitcherWhatsNew = () => {
const { t } = useTranslation()
return (
<div className="ide-redesign-switcher-modal-whats-new">
<h4>{t('whats_new')}</h4>
<ul>
<li>{t('chat')}</li>
<li>{t('settings_for_git_github_and_dropbox_integrations')}</li>
<li>{t('dark_mode')}</li>
</ul>
<hr />
<h4>{t('whats_next')}</h4>
<ul>
<li>{t('history')}</li>
<li>{t('review_panel_comments_and_track_changes')}</li>
</ul>
</div>
)
}
const LeavingNote = () => {
return (
<p className="ide-redesign-switcher-modal-leave-text">
<Trans
i18nKey="you_can_leave_the_experiment_from_your_account_settings_at_any_time"
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
components={[<a href="/user/settings" />]}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
)
}
@@ -0,0 +1,47 @@
import { useIdeRedesignSwitcherContext } from '@/features/ide-react/context/ide-redesign-switcher-context'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon from '@/shared/components/material-icon'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
export const LabsActions = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const openEditorRedesignSwitcherModal = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
return (
<>
<div className="ide-redesign-toolbar-button-container">
<OLTooltip
id="tooltip-labs-button"
description={t(
'this_is_a_labs_experiment_for_the_new_overleaf_editor_some_features_are_still_in_progress'
)}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<OLButton
size="sm"
variant="info"
className="ide-redesign-labs-button"
onClick={openEditorRedesignSwitcherModal}
leadingIcon={<MaterialIcon type="experiment" unfilled />}
>
{t('labs')}
</OLButton>
</OLTooltip>
</div>
<div className="ide-redesign-toolbar-button-container">
<a
href="https://forms.gle/soyVStc5qDx9na1Z6"
rel="noopener noreferrer"
target="_blank"
className="ide-redesign-toolbar-labs-feedback-link"
>
{t('give_feedback')}
</a>
</div>
</>
)
}
@@ -10,9 +10,18 @@ import {
import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
import { useTranslation } from 'react-i18next'
import ChangeLayoutOptions from './change-layout-options'
import { MouseEventHandler, useCallback } 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 '@/features/ui/components/ol/ol-spinner'
export const ToolbarMenuBar = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const openEditorRedesignSwitcherModal = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
return (
<MenuBar
className="ide-redesign-toolbar-menu-bar"
@@ -122,7 +131,41 @@ export const ToolbarMenuBar = () => {
<DropdownDivider />
<MenuBarOption title="Contact us" />
<MenuBarOption title="Give feedback" />
<DropdownDivider />
<SwitchToOldEditorMenuBarOption />
<MenuBarOption
title="What's new?"
onClick={openEditorRedesignSwitcherModal}
/>
</MenuBarDropdown>
</MenuBar>
)
}
const SwitchToOldEditorMenuBarOption = () => {
const { loading, error, setEditorRedesignStatus } =
useSwitchEnableNewEditorState()
const disable: MouseEventHandler = useCallback(
event => {
// Don't close the dropdown
event.stopPropagation()
setEditorRedesignStatus(false)
},
[setEditorRedesignStatus]
)
let icon = null
if (loading) {
icon = <OLSpinner size="sm" />
} else if (error) {
icon = <MaterialIcon type="error" title={error} className="text-danger" />
}
return (
<MenuBarOption
title="Switch to old editor"
onClick={disable}
disabled={loading}
trailingIcon={icon}
/>
)
}
@@ -6,6 +6,7 @@ import { OnlineUsers } from './online-users'
import ShareProjectButton from './share-project-button'
import ChangeLayoutButton from './change-layout-button'
import ShowHistoryButton from './show-history-button'
import { LabsActions } from './labs-actions'
export const Toolbar = () => {
return (
@@ -35,6 +36,7 @@ const ToolbarMenus = () => {
const ToolbarButtons = () => {
return (
<div className="ide-redesign-toolbar-actions">
<LabsActions />
<OnlineUsers />
<ShowHistoryButton />
<ChangeLayoutButton />
@@ -0,0 +1,34 @@
import { postJSON } from '@/infrastructure/fetch-json'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useCallback, useState } from 'react'
export const useSwitchEnableNewEditorState = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const { setUserSettings } = useUserSettingsContext()
const setEditorRedesignStatus = useCallback(
(status: boolean): Promise<void> => {
setLoading(true)
setError('')
return new Promise((resolve, reject) => {
postJSON('/user/settings', { body: { enableNewEditor: status } })
.then(() => {
setUserSettings(current => ({
...current,
enableNewEditor: status,
}))
resolve()
})
.catch(e => {
setError('Failed to update settings')
reject(e)
})
.finally(() => {
setLoading(false)
})
})
},
[setUserSettings]
)
return { loading, error, setEditorRedesignStatus }
}
@@ -0,0 +1,13 @@
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
// TODO: For now we're using the feature flag, but eventually we'll read this
// from labs.
export const canUseNewEditor = () => isSplitTestEnabled('editor-redesign')
export const useIsNewEditorEnabled = () => {
const { userSettings } = useUserSettingsContext()
const hasAccess = canUseNewEditor()
const enabled = userSettings.enableNewEditor
return hasAccess && enabled
}
@@ -8,16 +8,16 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co
import { PdfPreviewMessages } from './pdf-preview-messages'
import CompileTimeWarningUpgradePrompt from './compile-time-warning-upgrade-prompt'
import { PdfPreviewProvider } from './pdf-preview-provider'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import PdfPreviewHybridToolbarNew from '@/features/ide-redesign/components/pdf-preview/pdf-preview-hybrid-toolbar'
import PdfErrorState from '@/features/ide-redesign/components/pdf-preview/pdf-error-state'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
function PdfPreviewPane() {
const { pdfUrl, hasShortCompileTimeout } = useCompileContext()
const classes = classNames('pdf', 'full-size', {
'pdf-empty': !pdfUrl,
})
const newEditor = useFeatureFlag('editor-redesign')
const newEditor = useIsNewEditorEnabled()
return (
<div className={classes}>
@@ -1,17 +1,30 @@
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { useNestableDropdown } from '@/shared/hooks/use-nestable-dropdown'
import { MouseEventHandler, ReactNode } from 'react'
type MenuBarOptionProps = {
title: string
onClick?: () => void
onClick?: MouseEventHandler
disabled?: boolean
trailingIcon?: ReactNode
}
export const MenuBarOption = ({ title, onClick }: MenuBarOptionProps) => {
export const MenuBarOption = ({
title,
onClick,
disabled,
trailingIcon,
}: MenuBarOptionProps) => {
const { setSelected } = useNestableDropdown()
return (
<DropdownListItem>
<DropdownItem onMouseEnter={() => setSelected(null)} onClick={onClick}>
<DropdownItem
onMouseEnter={() => setSelected(null)}
onClick={onClick}
disabled={disabled}
trailingIcon={trailingIcon}
>
{title}
</DropdownItem>
</DropdownListItem>
@@ -27,6 +27,7 @@ const defaultSettings: UserSettings = {
lineHeight: 'normal',
mathPreview: true,
referencesSearchMode: 'advanced',
enableNewEditor: true,
}
type UserSettingsContextValue = {
@@ -7,6 +7,7 @@
@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';
@@ -0,0 +1,24 @@
.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;
}
.ide-redesign-switcher-modal-leave-text {
color: var(--content-secondary);
a {
color: var(--link-ui);
}
}
}
@@ -8,6 +8,7 @@
--redesign-toolbar-logo-url: url('../../../../../public/img/ol-brand/overleaf-o-white.svg');
--redesign-subdued-button-color: var(--content-primary-dark);
--redesign-subdued-button-hover-background: var(--bg-dark-tertiary);
--redesign-toolbar-feedback-link-color: var(--link-ui-dark);
}
@include theme('light') {
@@ -18,6 +19,7 @@
--redesign-toolbar-logo-url: url('../../../../../public/img/ol-brand/overleaf-o-dark.svg');
--redesign-subdued-button-color: var(--content-primary);
--redesign-subdued-button-hover-background: var(--bg-light-tertiary);
--redesign-toolbar-feedback-link-color: var(--link-ui);
}
.ide-redesign-toolbar {
@@ -149,3 +151,22 @@
max-width: 400px;
margin: var(--spacing-02) 0;
}
.ide-redesign-toolbar-labs-feedback-link {
&,
&:hover,
&:visited {
color: var(--redesign-toolbar-feedback-link-color);
}
}
.ide-redesign-labs-button.btn.btn-info {
@include ol-button-variant(
var(--content-positive),
var(--bg-accent-03),
var(--green-40),
var(--bg-accent-03),
var(--green-40),
false
);
}
@@ -94,7 +94,7 @@
.toolbar-right,
.toolbar-left {
button:not(.back-to-editor-btn) {
button:not(.back-to-editor-btn, .toolbar-experiment-button) {
background: transparent;
box-shadow: none;
}
@@ -120,7 +120,7 @@
.toolbar-left > a:not(.btn),
.toolbar-left > button,
.toolbar-right > a:not(.btn),
.toolbar-right > button:not(.back-to-editor-btn) {
.toolbar-right > button:not(.back-to-editor-btn, .toolbar-experiment-button) {
display: inline-block;
color: var(--toolbar-btn-color);
background-color: transparent;
@@ -502,3 +502,19 @@
.formatting-btn-icon:last-of-type {
border-right: 1px solid var(--formatting-btn-border);
}
.toolbar-experiment-button.btn.btn-info {
@include ol-button-variant(
var(--content-positive),
var(--bg-accent-03),
var(--green-40),
var(--bg-accent-03),
var(--green-40),
false
);
max-height: 39px;
font-size: var(--font-size-01);
line-height: var(--line-height-01);
margin-right: var(--spacing-04);
}
+16
View File
@@ -467,6 +467,7 @@
"customizing_figures": "Customizing figures",
"customizing_tables": "Customizing tables",
"da": "Danish",
"dark_mode": "Dark mode",
"date": "Date",
"date_and_owner": "Date and owner",
"de": "German",
@@ -916,6 +917,7 @@
"help_articles_matching": "Help articles matching your subject",
"help_improve_overleaf_fill_out_this_survey": "If you would like to help us improve Overleaf, please take a moment to fill out <0>this survey</0>.",
"help_improve_screen_reader_fill_out_this_survey": "Help us improve your experience using a screen reader with __appName__ by filling out this quick survey.",
"help_shape_the_future_of_overleaf": "Help shape the future of Overleaf",
"hide": "Hide",
"hide_configuration": "Hide configuration",
"hide_deleted_user": "Hide deleted users",
@@ -1118,6 +1120,7 @@
"ko": "Korean",
"labels_help_you_to_easily_reference_your_figures": "Labels help you to easily reference your figures throughout your document. To reference a figure within the text, reference the label using the <0>\\ref{...}</0> command. This makes it easy to reference figures without needing to manually remember the figure numbering. <1>Learn more</1>",
"labels_help_you_to_reference_your_tables": "Labels help you to reference your tables throughout your document easily. To reference a table within the text, reference the label using the <0>\\ref{...}</0> command. This makes it easy to reference tables without manually remembering the table numbering. <1>Read about labels and cross-references</1>.",
"labs": "Labs",
"labs_program_benefits": "By signing up for Overleaf Labs you can get your hands on in-development features and try them out as much as you like. All we ask in return is your honest feedback to help us develop and improve. Its important to note that features available in this program are still being tested and actively developed. This means they could change, be removed, or become part of a premium plan.",
"language": "Language",
"language_suggestions_for_texts_in_any_language": "</>Language suggestions<//> for texts in any language",
@@ -1847,6 +1850,7 @@
"reverse_x_sort_order": "Reverse __x__ sort order",
"revert_pending_plan_change": "Revert scheduled plan change",
"review": "Review",
"review_panel_comments_and_track_changes": "Review panel Comments & track changes",
"review_your_peers_work": "Review your peers work",
"reviewer": "Reviewer",
"reviewing": "Reviewing",
@@ -1973,6 +1977,7 @@
"set_up_single_sign_on": "Set up single sign-on (SSO)",
"set_up_sso": "Set up SSO",
"settings": "Settings",
"settings_for_git_github_and_dropbox_integrations": "Settings for Git, Github & Dropbox integrations",
"setup_another_account_under_a_personal_email_address": "Set up another Overleaf account under a personal email address.",
"share": "Share",
"share_project": "Share Project",
@@ -2140,6 +2145,8 @@
"switch_back_to_monthly_pay_20_more": "Switch back to monthly (20% more)",
"switch_plan": "Switch plan",
"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_standard_plan": "Switch to Standard plan",
"symbol_palette": "Symbol palette",
@@ -2195,6 +2202,7 @@
"thank_you_for_being_part_of_our_beta_program": "Thank you for being part of our Beta Program, where you can have <0>early access to new features</0> and help us understand your needs better",
"thank_you_for_your_feedback": "Thank you for your feedback!",
"thanks": "Thanks",
"thanks_for_being_part_of_this_labs_experiment_your_feedback_will_help_us_make_the_new_editor_the_best_yet": "Thanks for being part of this Labs experiment. Your feedback and support will help us make the new editor the best yet! Some features are still in progress, so if you need something thats missing, you can switch back to the old editor.",
"thanks_for_confirming_your_email_address": "Thanks for confirming your email address",
"thanks_for_getting_in_touch": "Thanks for getting in touch. Our team will get back to you by email as soon as possible.",
"thanks_for_subscribing": "Thanks for subscribing!",
@@ -2209,6 +2217,7 @@
"the_following_files_and_folders_already_exist_in_this_project": "The following files and folders already exist in this project:",
"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_new_overleaf_editor": "The new Overleaf editor",
"the_next_payment_will_be_collected_on": "The next payment will be collected on <strong>__date__</strong>.",
"the_original_text_has_changed": "The original text has changed, so this suggestion cant be applied",
"the_project_that_contains_this_file_is_not_shared_with_you": "The project that contains this file is not shared with you",
@@ -2236,6 +2245,7 @@
"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_labs_experiment_for_the_new_overleaf_editor_some_features_are_still_in_progress": "This is a Labs experiment for the new Overleaf editor. Some features are still in progress, so if you need something thats missing, 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",
@@ -2371,6 +2381,7 @@
"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_to_compile_despite_errors": "Try to compile despite errors",
"turn_off": "Turn off",
"turn_off_link_sharing": "Turn off link sharing",
@@ -2539,6 +2550,7 @@
"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.",
"were_redesigning_our_editor_to_make_it_easier_to_use": "Were redesigning our editor to make it easier to use and ensure its future ready. Try it out and give us your feedback to help us get this right. (Some features are still in the works, so you can switch back at any time.)",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected this project.",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected your project.",
"what_do_you_need": "What do you need?",
@@ -2547,6 +2559,8 @@
"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_new": "Whats new?",
"whats_next": "Whats next?",
"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?",
@@ -2595,6 +2609,7 @@
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are on our <0>__planName__</0> plan as a <1>member</1> of the group subscription <1>__groupName__</1> administered by <1>__adminEmail__</1>",
"you_can_also_choose_to_view_anonymously_or_leave_the_project": "You can also choose to <0>view anonymously</0> (you will lose edit access) or <1>leave the project</1>.",
"you_can_buy_this_plan_but_not_as_a_trial": "You can buy this plan but not as a trial, as youve completed a trial recently.",
"you_can_leave_the_experiment_from_your_account_settings_at_any_time": "You can leave the experiment from your <0>account settings</0> at any time.",
"you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "You can manage your reference manager integrations from your <0>account settings page</0>.",
"you_can_now_enable_sso": "You can now enable SSO on your Group settings page.",
"you_can_now_log_in_sso": "You can now log in through your institution and if eligible you will receive <0>__appName__ Professional features</0>.",
@@ -2672,6 +2687,7 @@
"youre_about_to_enable_single_sign_on_sso_only": "Youre about to enable single sign-on (SSO). Before you do this, you should ensure youre confident the SSO configuration is correct.",
"youre_adding_x_users_to_your_plan_giving_you_a_total_of_y_users": "Youre adding <0>__adding__</0> users to your plan giving you a total of <1>__total__</1> users.",
"youre_already_setup_for_sso": "Youre already set up for SSO",
"youre_helping_us_shape_the_future_of_overleaf": "Youre helping us shape the future of Overleaf",
"youre_joining": "Youre joining",
"youre_on_free_trial_which_ends_on": "Youre on a free trial which ends on <0>__date__</0>.",
"youre_signed_in_as_logout": "Youre signed in as <0>__email__</0>. <1>Log out.</1>",
@@ -452,6 +452,33 @@ describe('UserController', function () {
this.UserController.updateUserSettings(this.req, this.res)
})
it('should set enableNewEditor to true', function (done) {
this.req.body = { enableNewEditor: true }
this.res.sendStatus = code => {
this.user.ace.enableNewEditor.should.equal(true)
done()
}
this.UserController.updateUserSettings(this.req, this.res)
})
it('should set enableNewEditor to false', function (done) {
this.req.body = { enableNewEditor: false }
this.res.sendStatus = code => {
this.user.ace.enableNewEditor.should.equal(false)
done()
}
this.UserController.updateUserSettings(this.req, this.res)
})
it('should keep enableNewEditor a boolean', function (done) {
this.req.body = { enableNewEditor: 'foobar' }
this.res.sendStatus = code => {
this.user.ace.enableNewEditor.should.equal(true)
done()
}
this.UserController.updateUserSettings(this.req, this.res)
})
it('should send an error if the email is 0 len', function (done) {
this.req.body.email = ''
this.res.sendStatus = function (code) {
+1
View File
@@ -16,4 +16,5 @@ export type UserSettings = {
lineHeight: LineHeight
mathPreview: boolean
referencesSearchMode: 'advanced' | 'simple'
enableNewEditor: boolean
}