From 062b2d57f8b345742a93fdeca83d8b424f40318e Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:08:07 +0000 Subject: [PATCH] Merge pull request #24137 from overleaf/mj-ide-permissions-utils [web] Add switcher for editor redesign GitOrigin-RevId: 806a1f567027df53f879b564a50aaae9166c8480 --- .../src/Features/Project/ProjectController.js | 1 + .../app/src/Features/User/UserController.js | 3 + services/web/app/src/models/User.js | 1 + .../web/frontend/extracted-translations.json | 16 ++ ...alSymbolsRoundedUnfilledPartialSlice.woff2 | Bin 4156 -> 4392 bytes .../material-symbols/unfilled-symbols.mjs | 1 + .../components/toolbar-header.jsx | 4 + .../try-new-editor-button.tsx | 28 +++ .../components/file-tree-folder-icons.tsx | 5 +- .../file-tree/components/file-tree-icon.tsx | 4 +- .../file-tree/components/file-tree-root.tsx | 4 +- .../ide-react/components/layout/ide-page.tsx | 4 +- .../ide-react/components/modals/modals.tsx | 2 + .../context/ide-redesign-switcher-context.tsx | 39 ++++ .../ide-react/context/react-context-root.tsx | 6 +- .../pdf-preview/pdf-error-state.tsx | 4 +- .../components/switcher-modal/modal.tsx | 178 ++++++++++++++++++ .../components/toolbar/labs-actions.tsx | 47 +++++ .../components/toolbar/menu-bar.tsx | 43 +++++ .../components/toolbar/toolbar.tsx | 2 + .../use-switch-enable-new-editor-state.ts | 34 ++++ .../ide-redesign/utils/new-editor-utils.ts | 13 ++ .../components/pdf-preview-pane.tsx | 4 +- .../components/menu-bar/menu-bar-option.tsx | 19 +- .../shared/context/user-settings-context.tsx | 1 + .../stylesheets/bootstrap-5/pages/all.scss | 1 + .../editor/ide-redesign-switcher-modal.scss | 24 +++ .../pages/editor/toolbar-redesign.scss | 21 +++ .../bootstrap-5/pages/editor/toolbar.scss | 20 +- services/web/locales/en.json | 16 ++ .../test/unit/src/User/UserControllerTests.js | 27 +++ services/web/types/user-settings.ts | 1 + 32 files changed, 554 insertions(+), 19 deletions(-) create mode 100644 services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx create mode 100644 services/web/frontend/js/features/ide-react/context/ide-redesign-switcher-context.tsx create mode 100644 services/web/frontend/js/features/ide-redesign/components/switcher-modal/modal.tsx create mode 100644 services/web/frontend/js/features/ide-redesign/components/toolbar/labs-actions.tsx create mode 100644 services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts create mode 100644 services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts create mode 100644 services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide-redesign-switcher-modal.scss diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 8ff63d2593..0c6a28d57d 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -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, diff --git a/services/web/app/src/Features/User/UserController.js b/services/web/app/src/Features/User/UserController.js index d4257f7912..e4186d39a8 100644 --- a/services/web/app/src/Features/User/UserController.js +++ b/services/web/app/src/Features/User/UserController.js @@ -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() diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js index 41e502c4d8..73506f161c 100644 --- a/services/web/app/src/models/User.js +++ b/services/web/app/src/models/User.js @@ -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: { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 431bf83252..c8864d8b2a 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 index ade3a5b5df989911474a56e8c079f5042abad467..14a5ef1e2bb15e116072c39297e63a01e822b47c 100644 GIT binary patch literal 4392 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
+ {canUseNewEditor() && } + {historyIsOpen ? ( diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx new file mode 100644 index 0000000000..91cc150ff6 --- /dev/null +++ b/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx @@ -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 ( +
+ } + variant="info" + > + {t('try_the_new_editor')} + +
+ ) +} + +export default TryNewEditorButton diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder-icons.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-folder-icons.tsx index 0248a3fe85..45099e69fe 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder-icons.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-folder-icons.tsx @@ -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 ( diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-icon.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-icon.tsx index 483c84301d..907bfdc44b 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-icon.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-icon.tsx @@ -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 ( diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx index 24db6255f9..561e1b5e1d 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx @@ -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) { diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx index 22f9fc0da4..fa235560e8 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx @@ -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 ( diff --git a/services/web/frontend/js/features/ide-react/components/modals/modals.tsx b/services/web/frontend/js/features/ide-react/components/modals/modals.tsx index ed031dcf1d..eff07ae89a 100644 --- a/services/web/frontend/js/features/ide-react/components/modals/modals.tsx +++ b/services/web/frontend/js/features/ide-react/components/modals/modals.tsx @@ -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(() => { + ) }) diff --git a/services/web/frontend/js/features/ide-react/context/ide-redesign-switcher-context.tsx b/services/web/frontend/js/features/ide-react/context/ide-redesign-switcher-context.tsx new file mode 100644 index 0000000000..464812d663 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/ide-redesign-switcher-context.tsx @@ -0,0 +1,39 @@ +import { + createContext, + Dispatch, + FC, + SetStateAction, + useContext, + useState, +} from 'react' + +type IdeRedesignSwitcherContextValue = { + showSwitcherModal: boolean + setShowSwitcherModal: Dispatch> +} + +export const IdeRedesignSwitcherContext = createContext< + IdeRedesignSwitcherContextValue | undefined +>(undefined) + +export const IdeRedesignSwitcherProvider: FC = ({ children }) => { + const [showSwitcherModal, setShowSwitcherModal] = useState(false) + + return ( + + {children} + + ) +} + +export const useIdeRedesignSwitcherContext = () => { + const context = useContext(IdeRedesignSwitcherContext) + if (!context) { + throw new Error( + 'useIdeRedesignSwitcherContext is only available inside IdeRedesignSwitcherProvider' + ) + } + return context +} diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx index 9ec848dbf6..9551777813 100644 --- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx +++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx @@ -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 }> = ({ children, @@ -55,6 +56,7 @@ export const ReactContextRoot: FC<{ providers?: Record }> = ({ SplitTestProvider, UserProvider, UserSettingsProvider, + IdeRedesignSwitcherProvider, ...providers, } @@ -84,7 +86,9 @@ export const ReactContextRoot: FC<{ providers?: Record }> = ({ - {children} + + {children} + diff --git a/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx index cb87774945..2de51dc11e 100644 --- a/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx @@ -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 diff --git a/services/web/frontend/js/features/ide-redesign/components/switcher-modal/modal.tsx b/services/web/frontend/js/features/ide-redesign/components/switcher-modal/modal.tsx new file mode 100644 index 0000000000..11807e1a53 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/switcher-modal/modal.tsx @@ -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 ( + + + {t('the_new_overleaf_editor')} + + {error && } + + + ) +} + +type ModalContentProps = { + setEditorRedesignStatus: (enabled: boolean) => Promise + hide: () => void + loading: boolean +} + +const SwitcherModalContentEnabled: FC = ({ + 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 ( + <> + +

{t('youre_helping_us_shape_the_future_of_overleaf')}

+

+ {t( + 'thanks_for_being_part_of_this_labs_experiment_your_feedback_will_help_us_make_the_new_editor_the_best_yet' + )} +

+ + +
+ + + {t('switch_to_old_editor')} + + + {t('cancel')} + + + {t('give_feedback')} + + + + ) +} + +const SwitcherModalContentDisabled: FC = ({ + 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 ( + <> + +

{t('help_shape_the_future_of_overleaf')}

+

{t('were_redesigning_our_editor_to_make_it_easier_to_use')}

+ + +
+ + + {t('cancel')} + + + {t('switch_to_new_editor')} + + + + ) +} + +const SwitcherWhatsNew = () => { + const { t } = useTranslation() + return ( +
+

{t('whats_new')}

+
    +
  • {t('chat')}
  • +
  • {t('settings_for_git_github_and_dropbox_integrations')}
  • +
  • {t('dark_mode')}
  • +
+
+

{t('whats_next')}

+
    +
  • {t('history')}
  • +
  • {t('review_panel_comments_and_track_changes')}
  • +
+
+ ) +} + +const LeavingNote = () => { + return ( +

+ ]} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> +

+ ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/labs-actions.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/labs-actions.tsx new file mode 100644 index 0000000000..735c0cfd27 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/labs-actions.tsx @@ -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 ( + <> +
+ + } + > + {t('labs')} + + +
+ + + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx index 472dcf8baf..e28fcc34a7 100644 --- a/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx @@ -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 ( { + + + ) } + +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 = + } else if (error) { + icon = + } + return ( + + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx index 86f948038b..e9c05cbffc 100644 --- a/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx @@ -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 (
+ diff --git a/services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts b/services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts new file mode 100644 index 0000000000..c32578591f --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts @@ -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 => { + 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 } +} diff --git a/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts new file mode 100644 index 0000000000..5dbffab7d5 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts @@ -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 +} diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx index a1d23c5e6f..8133ff31ee 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx @@ -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 (
diff --git a/services/web/frontend/js/shared/components/menu-bar/menu-bar-option.tsx b/services/web/frontend/js/shared/components/menu-bar/menu-bar-option.tsx index 916dd79f80..20cca2135b 100644 --- a/services/web/frontend/js/shared/components/menu-bar/menu-bar-option.tsx +++ b/services/web/frontend/js/shared/components/menu-bar/menu-bar-option.tsx @@ -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 ( - setSelected(null)} onClick={onClick}> + setSelected(null)} + onClick={onClick} + disabled={disabled} + trailingIcon={trailingIcon} + > {title} diff --git a/services/web/frontend/js/shared/context/user-settings-context.tsx b/services/web/frontend/js/shared/context/user-settings-context.tsx index 6a069f470e..b9b6f22ad1 100644 --- a/services/web/frontend/js/shared/context/user-settings-context.tsx +++ b/services/web/frontend/js/shared/context/user-settings-context.tsx @@ -27,6 +27,7 @@ const defaultSettings: UserSettings = { lineHeight: 'normal', mathPreview: true, referencesSearchMode: 'advanced', + enableNewEditor: true, } type UserSettingsContextValue = { diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss index 87c34f04fa..9b3ec278dc 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss @@ -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'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide-redesign-switcher-modal.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide-redesign-switcher-modal.scss new file mode 100644 index 0000000000..ba2f2aa49d --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide-redesign-switcher-modal.scss @@ -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); + } + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar-redesign.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar-redesign.scss index 9a9a7e271d..87185ab3ed 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar-redesign.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar-redesign.scss @@ -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 + ); +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss index 0e7aaaeca9..70b5dac91c 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss @@ -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); +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index c293511896..dd743ad959 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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.", "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{...} command. This makes it easy to reference figures without needing to manually remember the figure numbering. <1>Learn more", "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{...} command. This makes it easy to reference tables without manually remembering the table numbering. <1>Read about labels and cross-references.", + "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. It’s 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 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 that’s 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 __date__.", "the_original_text_has_changed": "The original text has changed, so this suggestion can’t 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 isn’t 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>__featureType__ 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 that’s 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 doesn’t help, follow our <0>troubleshooting guide.", "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": "We’ll be here when you’re ready to dive back in! 🦆", "were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "We’re making some <0>changes to project sharing. 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": "We’re 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": "We’re redesigning our editor to make it easier to use and ensure it’s 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": "We’ve recently <0>reduced the compile timeout limit on our free plan, which may have affected this project.", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "We’ve recently <0>reduced the compile timeout limit 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": "What’s new?", + "whats_next": "What’s next?", "when_you_join_labs": "When you join Labs, you can choose which experiments you want to be part of. Once you’ve done that, you can use Overleaf as normal, but you’ll 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__ plan as a <1>member of the group subscription <1>__groupName__ administered by <1>__adminEmail__", "you_can_also_choose_to_view_anonymously_or_leave_the_project": "You can also choose to <0>view anonymously (you will lose edit access) or <1>leave the project.", "you_can_buy_this_plan_but_not_as_a_trial": "You can buy this plan but not as a trial, as you’ve 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 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.", "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.", @@ -2672,6 +2687,7 @@ "youre_about_to_enable_single_sign_on_sso_only": "You’re about to enable single sign-on (SSO). Before you do this, you should ensure you’re confident the SSO configuration is correct.", "youre_adding_x_users_to_your_plan_giving_you_a_total_of_y_users": "You’re adding <0>__adding__ users to your plan giving you a total of <1>__total__ users.", "youre_already_setup_for_sso": "You’re already set up for SSO", + "youre_helping_us_shape_the_future_of_overleaf": "You’re helping us shape the future of Overleaf", "youre_joining": "You’re joining", "youre_on_free_trial_which_ends_on": "You’re on a free trial which ends on <0>__date__.", "youre_signed_in_as_logout": "You’re signed in as <0>__email__. <1>Log out.", diff --git a/services/web/test/unit/src/User/UserControllerTests.js b/services/web/test/unit/src/User/UserControllerTests.js index 98ae8db73e..8dc8f034b7 100644 --- a/services/web/test/unit/src/User/UserControllerTests.js +++ b/services/web/test/unit/src/User/UserControllerTests.js @@ -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) { diff --git a/services/web/types/user-settings.ts b/services/web/types/user-settings.ts index 2753e09777..3e748d937e 100644 --- a/services/web/types/user-settings.ts +++ b/services/web/types/user-settings.ts @@ -16,4 +16,5 @@ export type UserSettings = { lineHeight: LineHeight mathPreview: boolean referencesSearchMode: 'advanced' | 'simple' + enableNewEditor: boolean }