diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index a2b2483c85..6d9ef11aed 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -530,14 +530,12 @@ "editing": "", "editing_captions": "", "editor": "", - "editor_and_pdf": "", "editor_disconected_click_to_reconnect": "", "editor_font_family": "", "editor_font_size": "", "editor_limit_exceeded_in_this_project": "", "editor_line_height": "", "editor_only": "", - "editor_only_hide_pdf": "", "editor_theme": "", "editor_theme_dark": "", "editor_theme_light": "", @@ -982,9 +980,7 @@ "latex_in_thirty_minutes": "", "latex_places_figures_according_to_a_special_algorithm": "", "latex_places_tables_according_to_a_special_algorithm": "", - "layout": "", "layout_options": "", - "layout_processing": "", "learn_more": "", "learn_more_about": "", "learn_more_about_account": "", @@ -1300,9 +1296,7 @@ "pdf_compile_in_progress_error": "", "pdf_compile_rate_limit_hit": "", "pdf_compile_try_again": "", - "pdf_in_separate_tab": "", "pdf_only": "", - "pdf_only_hide_editor": "", "pdf_preview": "", "pdf_preview_error": "", "pdf_rendering_error": "", @@ -1843,7 +1837,6 @@ "switch_easily_between_your_files_comments_track_changes_and_more": "", "switch_to_editor": "", "switch_to_new_editor_design": "", - "switch_to_new_look": "", "switch_to_pdf": "", "switch_to_standard_plan": "", "symbol": "", @@ -2309,7 +2302,6 @@ "your_account_is_managed_by_your_group_admin": "", "your_add_on_has_been_cancelled_and_will_remain_active_until_your_billing_cycle_ends_on": "", "your_affiliation_is_confirmed": "", - "your_browser_does_not_support_this_feature": "", "your_changes_will_save": "", "your_compile_timed_out": "", "your_current_plan": "", diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.tsx deleted file mode 100644 index 8185060cfc..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useTranslation } from 'react-i18next' -import * as eventTracking from '../../../infrastructure/event-tracking' -import OLTooltip from '@/shared/components/ol/ol-tooltip' -import MaterialIcon from '@/shared/components/material-icon' - -function BackToProjectsButton() { - const { t } = useTranslation() - - return ( - -
- { - eventTracking.sendMB('navigation-clicked-home') - }} - > - - -
-
- ) -} - -export default BackToProjectsButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button.tsx deleted file mode 100644 index 490548b8a7..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import classNames from 'classnames' -import { useTranslation } from 'react-i18next' -import MaterialIcon from '@/shared/components/material-icon' -import OLBadge from '@/shared/components/ol/ol-badge' - -function ChatToggleButton({ - chatIsOpen, - unreadMessageCount, - onClick, -}: { - chatIsOpen: boolean - unreadMessageCount: number - onClick: () => void -}) { - const { t } = useTranslation() - const classes = classNames('btn', 'btn-full-height', { active: chatIsOpen }) - - const hasUnreadMessages = unreadMessageCount > 0 - - return ( -
- -
- ) -} - -export default ChatToggleButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/cobranding-logo.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/cobranding-logo.tsx deleted file mode 100644 index 56d9f7b6ab..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/cobranding-logo.tsx +++ /dev/null @@ -1,26 +0,0 @@ -function CobrandingLogo({ - brandVariationHomeUrl, - brandVariationName, - logoImgUrl, -}: { - brandVariationHomeUrl: string - brandVariationName: string - logoImgUrl: string -}) { - return ( - - {brandVariationName} - - ) -} - -export default CobrandingLogo diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.tsx deleted file mode 100644 index 093296e476..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useTranslation } from 'react-i18next' -import MaterialIcon from '@/shared/components/material-icon' - -function HistoryToggleButton({ onClick }: { onClick: () => void }) { - const { t } = useTranslation() - - return ( -
- -
-
- ) -} - -export default HistoryToggleButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.tsx deleted file mode 100644 index 60e732ad40..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { memo, forwardRef } from 'react' -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownToggle, - DropdownToggleCustom, -} from '@/shared/components/dropdown/dropdown-menu' -import { Trans, useTranslation } from 'react-i18next' -import { - IdeLayout, - IdeView, - useLayoutContext, -} from '../../../shared/context/layout-context' -import * as eventTracking from '../../../infrastructure/event-tracking' -import { DetachRole } from '@/shared/context/detach-context' -import MaterialIcon from '@/shared/components/material-icon' -import OLTooltip from '@/shared/components/ol/ol-tooltip' -import OLSpinner from '@/shared/components/ol/ol-spinner' - -const isActiveDropdownItem = ({ - iconFor, - pdfLayout, - view, - detachRole, -}: { - iconFor: string - pdfLayout: IdeLayout - view: IdeView | null - detachRole?: DetachRole -}) => { - if (detachRole === 'detacher' || view === 'history') { - return false - } - if ( - iconFor === 'editorOnly' && - pdfLayout === 'flat' && - (view === 'editor' || view === 'file') - ) { - return true - } else if (iconFor === 'pdfOnly' && pdfLayout === 'flat' && view === 'pdf') { - return true - } else if (iconFor === 'sideBySide' && pdfLayout === 'sideBySide') { - return true - } - return false -} - -function EnhancedDropdownItem({ - active, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -const LayoutDropdownToggleButton = forwardRef< - HTMLButtonElement, - { - onClick: (e: React.MouseEvent) => void - } ->(({ onClick, ...props }, ref) => { - const handleClick = (e: React.MouseEvent) => { - eventTracking.sendMB('navigation-clicked-layout') - onClick(e) - } - - return -}) -LayoutDropdownToggleButton.displayName = 'LayoutDropdownToggleButton' - -function BS5DetachDisabled() { - const { t } = useTranslation() - - return ( - - - - {t('pdf_in_separate_tab')} - - - - ) -} - -function LayoutDropdownButton() { - const { - detachIsLinked, - detachRole, - view, - pdfLayout, - handleChangeLayout, - handleDetach, - } = useLayoutContext() - - return ( - - ) -} - -type LayoutDropdownButtonUiProps = { - processing: boolean - handleChangeLayout: (newLayout: IdeLayout, newView?: IdeView) => void - handleDetach: () => void - detachIsLinked: boolean - detachRole: DetachRole - pdfLayout: IdeLayout - view: IdeView | null - detachable: boolean -} - -export const LayoutDropdownButtonUi = ({ - processing, - handleChangeLayout, - handleDetach, - detachIsLinked, - detachRole, - view, - pdfLayout, - detachable, -}: LayoutDropdownButtonUiProps) => { - const { t } = useTranslation() - return ( - <> -
- {processing ? t('layout_processing') : ''} -
- - - {processing ? ( - - ) : ( - - )} - {t('layout')} - - - handleChangeLayout('sideBySide')} - active={isActiveDropdownItem({ - iconFor: 'sideBySide', - pdfLayout, - view, - detachRole, - })} - leadingIcon="dock_to_right" - > - {t('editor_and_pdf')} - - - handleChangeLayout('flat', 'editor')} - active={isActiveDropdownItem({ - iconFor: 'editorOnly', - pdfLayout, - view, - detachRole, - })} - leadingIcon="code" - > -
- , - ]} - /> -
-
- - handleChangeLayout('flat', 'pdf')} - active={isActiveDropdownItem({ - iconFor: 'pdfOnly', - pdfLayout, - view, - detachRole, - })} - leadingIcon="picture_as_pdf" - > -
- , - ]} - /> -
-
- - {detachable ? ( - handleDetach()} - active={detachRole === 'detacher' && detachIsLinked} - trailingIcon={ - detachRole === 'detacher' ? ( - detachIsLinked ? ( - 'check' - ) : ( - - ) - ) : null - } - leadingIcon="select_window" - > - {t('pdf_in_separate_tab')} - - ) : ( - - )} -
-
- - ) -} - -export default memo(LayoutDropdownButton) diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/menu-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/menu-button.tsx deleted file mode 100644 index fb059737dd..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/menu-button.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useTranslation } from 'react-i18next' -import MaterialIcon from '@/shared/components/material-icon' - -function MenuButton({ onClick }: { onClick: () => void }) { - const { t } = useTranslation() - - return ( -
- -
- ) -} - -export default MenuButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.tsx index 991e3d00ef..ae3f1d58b3 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.tsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.tsx @@ -1,5 +1,4 @@ -import React from 'react' -import { useTranslation } from 'react-i18next' +import { OnlineUser } from '@/features/ide-react/context/online-users-context' import { Dropdown, DropdownHeader, @@ -7,125 +6,141 @@ import { DropdownMenu, DropdownToggle, } from '@/shared/components/dropdown/dropdown-menu' -import { getBackgroundColorForUserId } from '@/shared/utils/colors' import OLTooltip from '@/shared/components/ol/ol-tooltip' -import MaterialIcon from '@/shared/components/material-icon' -import { OnlineUser } from '@/features/ide-react/context/online-users-context' +import { + getBackgroundColorForUserId, + hslStringToLuminance, +} from '@/shared/utils/colors' +import classNames from 'classnames' +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' import { Doc } from '@ol-types/doc' -function OnlineUsersWidget({ +// Should be kept in sync with $max-user-circles-displayed CSS constant +const MAX_USER_CIRCLES_DISPLAYED = 5 + +// We don't want a +1 circle since we could just show the user instead +const MAX_USERS_WITH_OVERFLOW_VISIBLE = MAX_USER_CIRCLES_DISPLAYED - 1 + +export const OnlineUsersWidget = ({ onlineUsers, goToUser, }: { onlineUsers: OnlineUser[] goToUser: (user: OnlineUser) => Promise -}) { - const { t } = useTranslation() - - const shouldDisplayDropdown = onlineUsers.length >= 4 - - if (shouldDisplayDropdown) { - return ( - - - - - {onlineUsers.map((user, index) => ( -
  • - goToUser(user)} - > - - -
  • - ))} -
    -
    - ) - } else { - return ( -
    - {onlineUsers.map((user, index) => ( - - - {/* OverlayTrigger won't fire unless UserIcon is wrapped in a span */} - - - - ))} -
    - ) - } -} - -function UserIcon({ - user, - showName, - onClick, -}: { - user: OnlineUser - showName?: boolean - onClick?: (user: OnlineUser) => void -}) { - const backgroundColor = getBackgroundColorForUserId(user.user_id) - - function handleOnClick() { - onClick?.(user) - } - - const [character] = [...user.name] +}) => { + const hasOverflow = onlineUsers.length > MAX_USER_CIRCLES_DISPLAYED + const usersBeforeOverflow = useMemo( + () => + hasOverflow + ? onlineUsers.slice(0, MAX_USERS_WITH_OVERFLOW_VISIBLE) + : onlineUsers, + [onlineUsers, hasOverflow] + ) + const usersInOverflow = useMemo( + () => + hasOverflow ? onlineUsers.slice(MAX_USERS_WITH_OVERFLOW_VISIBLE) : [], + [onlineUsers, hasOverflow] + ) return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions - - - {character} - - {showName && user.name} +
    + {usersBeforeOverflow.map((user, index) => ( + + ))} + {hasOverflow && ( + + )} +
    + ) +} + +const OnlineUserWidget = ({ + user, + goToUser, + id, +}: { + user: OnlineUser + goToUser: (user: OnlineUser) => void + id: string +}) => { + const onClick = useCallback(() => { + goToUser(user) + }, [goToUser, user]) + return ( + + + + ) +} + +const OnlineUserCircle = ({ user }: { user: OnlineUser }) => { + const backgroundColor = getBackgroundColorForUserId(user.user_id) + const luminance = hslStringToLuminance(backgroundColor) + const [character] = [...user.name] + return ( + = 0.5, + })} + style={{ backgroundColor }} + > + {character} ) } -const DropDownToggleButton = React.forwardRef< - HTMLButtonElement, - { onlineUserCount: number; onClick: React.MouseEventHandler } ->((props, ref) => { +const OnlineUserOverflow = ({ + goToUser, + users, +}: { + goToUser: (user: OnlineUser) => void + users: OnlineUser[] +}) => { const { t } = useTranslation() return ( - - - + + + + +{users.length} + + + + + {users.map((user, index) => ( +
  • + goToUser(user)} + > + {user.name} + +
  • + ))} +
    +
    ) -}) - -DropDownToggleButton.displayName = 'DropDownToggleButton' - -export default OnlineUsersWidget +} diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.tsx deleted file mode 100644 index 04a0143962..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useEffect, useState, useRef } from 'react' -import { useTranslation } from 'react-i18next' -import classNames from 'classnames' -import OLFormControl from '@/shared/components/ol/ol-form-control' -import OLTooltip from '@/shared/components/ol/ol-tooltip' -import MaterialIcon from '@/shared/components/material-icon' - -type ProjectNameEditableLabelProps = { - projectName: string - onChange: (value: string) => void - hasRenamePermissions?: boolean - className?: string -} - -function ProjectNameEditableLabel({ - projectName, - hasRenamePermissions, - onChange, - className, -}: ProjectNameEditableLabelProps) { - const { t } = useTranslation() - - const [isRenaming, setIsRenaming] = useState(false) - - const canRename = hasRenamePermissions && !isRenaming - - const [inputContent, setInputContent] = useState(projectName) - - const inputRef = useRef(null) - - useEffect(() => { - if (isRenaming) { - inputRef.current?.select() - } - }, [isRenaming]) - - function startRenaming() { - if (canRename) { - setInputContent(projectName) - setIsRenaming(true) - } - } - - function finishRenaming() { - setIsRenaming(false) - onChange(inputContent) - } - - function handleKeyDown(event: React.KeyboardEvent) { - if (event.key === 'Enter') { - event.preventDefault() - finishRenaming() - } - } - - function handleOnChange(event: React.ChangeEvent) { - setInputContent(event.target.value) - } - - function handleBlur() { - if (isRenaming) { - finishRenaming() - } - } - - return ( -
    - {!isRenaming && ( - - {projectName} - - )} - {isRenaming && ( - - )} - {canRename && ( - - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */} - - - - - )} -
    - ) -} - -export default ProjectNameEditableLabel diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.tsx deleted file mode 100644 index 359ce92ffb..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useTranslation } from 'react-i18next' -import MaterialIcon from '@/shared/components/material-icon' - -function ShareProjectButton({ onClick }: { onClick: () => void }) { - const { t } = useTranslation() - - return ( -
    - -
    -
    - ) -} - -export default ShareProjectButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx deleted file mode 100644 index 2393680516..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { ElementType } from 'react' -import { useTranslation } from 'react-i18next' -import MenuButton from './menu-button' -import CobrandingLogo from './cobranding-logo' -import BackToProjectsButton from './back-to-projects-button' -import UpgradePrompt from './upgrade-prompt' -import ChatToggleButton from './chat-toggle-button' -import LayoutDropdownButton from './layout-dropdown-button' -import OnlineUsersWidget from './online-users-widget' -import ProjectNameEditableLabel from './project-name-editable-label' -import TrackChangesToggleButton from './track-changes-toggle-button' -import HistoryToggleButton from './history-toggle-button' -import ShareProjectButton from './share-project-button' -import importOverleafModules from '../../../../macros/import-overleaf-module.macro' -import BackToEditorButton from './back-to-editor-button' -import getMeta from '@/utils/meta' -import { isSplitTestEnabled } from '@/utils/splitTestUtils' -import TryNewEditorButton from '../try-new-editor-button' -import { OnlineUser } from '@/features/ide-react/context/online-users-context' -import { Cobranding } from '../../../../../types/cobranding' -import { canUseNewEditor } from '@/features/ide-redesign/utils/new-editor-utils' -import { Doc } from '@ol-types/doc' - -const [publishModalModules] = importOverleafModules('publishModal') as { - import: { default: ElementType } - path: string -}[] -const PublishButton = publishModalModules?.import.default - -const offlineModeToolbarButtons = importOverleafModules( - 'offlineModeToolbarButtons' -) as { - import: { default: ElementType } - path: string -}[] - -// double opt-in -const enableROMirrorOnClient = - isSplitTestEnabled('ro-mirror-on-client') && - new URLSearchParams(window.location.search).get('ro-mirror-on-client') === - 'enabled' - -export type ToolbarHeaderProps = { - cobranding: Cobranding | undefined - onShowLeftMenuClick: () => void - chatIsOpen: boolean - toggleChatOpen: () => void - reviewPanelOpen: boolean - toggleReviewPanelOpen: (e: React.MouseEvent) => void - historyIsOpen: boolean - toggleHistoryOpen: () => void - unreadMessageCount: number - onlineUsers: OnlineUser[] - goToUser: (user: OnlineUser) => Promise - isRestrictedTokenMember: boolean | undefined - hasPublishPermissions: boolean - chatVisible: boolean - projectName: string - renameProject: (name: string) => void - hasRenamePermissions: boolean - openShareModal: () => void - trackChangesVisible: boolean | undefined -} - -const ToolbarHeader = React.memo(function ToolbarHeader({ - cobranding, - onShowLeftMenuClick, - chatIsOpen, - toggleChatOpen, - reviewPanelOpen, - toggleReviewPanelOpen, - historyIsOpen, - toggleHistoryOpen, - unreadMessageCount, - onlineUsers, - goToUser, - isRestrictedTokenMember, - hasPublishPermissions, - chatVisible, - projectName, - renameProject, - hasRenamePermissions, - openShareModal, - trackChangesVisible, -}: ToolbarHeaderProps) { - const chatEnabled = getMeta('ol-capabilities')?.includes('chat') - - const { t } = useTranslation() - const shouldDisplayPublishButton = hasPublishPermissions && PublishButton - - return ( - - ) -}) - -export default ToolbarHeader diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.tsx deleted file mode 100644 index 927e6ec2c5..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import classNames from 'classnames' -import { useTranslation } from 'react-i18next' -import MaterialIcon from '@/shared/components/material-icon' - -function TrackChangesToggleButton({ - trackChangesIsOpen, - disabled, - onMouseDown, -}: { - trackChangesIsOpen: boolean - disabled?: boolean - onMouseDown: (e: React.MouseEvent) => void -}) { - const { t } = useTranslation() - const classes = classNames('btn', 'btn-full-height', { - active: trackChangesIsOpen && !disabled, - disabled, - }) - - return ( -
    - -
    -
    - ) -} - -export default TrackChangesToggleButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/upgrade-prompt.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/upgrade-prompt.tsx deleted file mode 100644 index d3cc775af9..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/upgrade-prompt.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useTranslation } from 'react-i18next' -import * as eventTracking from '../../../infrastructure/event-tracking' -import OLButton from '@/shared/components/ol/ol-button' - -function UpgradePrompt() { - const { t } = useTranslation() - - function handleClick() { - eventTracking.send('subscription-funnel', 'code-editor', 'upgrade') - eventTracking.sendMB('upgrade-button-click', { source: 'code-editor' }) - } - - return ( - - {t('upgrade')} - - ) -} - -export default UpgradePrompt 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 deleted file mode 100644 index d573d8813c..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useCallback, useState } from 'react' -import OLButton from '../../shared/components/ol/ol-button' -import { useTranslation } from 'react-i18next' -import { useSwitchEnableNewEditorState } from '../ide-redesign/hooks/use-switch-enable-new-editor-state' -import MaterialIcon from '@/shared/components/material-icon' -import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics' -import OldEditorWarningTooltip from '../ide-redesign/components/old-editor-warning-tooltip' - -const TryNewEditorButton = () => { - const { t } = useTranslation() - const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState() - const { sendEvent } = useEditorAnalytics() - const [buttonElt, setButtonElt] = useState(null) - const buttonRef = useCallback((node: HTMLButtonElement) => { - if (node !== null) { - setButtonElt(node) - } - }, []) - - const onClick = useCallback(() => { - sendEvent('switch-to-new-editor', { - location: 'toolbar', - }) - setEditorRedesignStatus(true) - }, [setEditorRedesignStatus, sendEvent]) - - return ( -
    - - - {t('switch_to_new_look')} - - -
    - ) -} - -export default TryNewEditorButton diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/online-users.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/online-users.tsx index 70cc330c0b..9a3352901e 100644 --- a/services/web/frontend/js/features/ide-react/components/toolbar/online-users.tsx +++ b/services/web/frontend/js/features/ide-react/components/toolbar/online-users.tsx @@ -4,7 +4,7 @@ import { useOnlineUsersContext, } from '@/features/ide-react/context/online-users-context' import { useCallback } from 'react' -import { OnlineUsersWidget } from '@/features/ide-redesign/components/online-users/online-users-widget' +import { OnlineUsersWidget } from '@/features/editor-navigation-toolbar/components/online-users-widget' export const OnlineUsers = () => { const { openDoc } = useEditorManagerContext() diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx index 94f019a7a1..0cd54e9d36 100644 --- a/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx +++ b/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx @@ -59,15 +59,13 @@ export const ToolbarProjectTitle = () => { {name} - + {shouldDisplaySubmitButton && !cobranding && ( diff --git a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx deleted file mode 100644 index ae3f1d58b3..0000000000 --- a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { OnlineUser } from '@/features/ide-react/context/online-users-context' -import { - Dropdown, - DropdownHeader, - DropdownItem, - DropdownMenu, - DropdownToggle, -} from '@/shared/components/dropdown/dropdown-menu' -import OLTooltip from '@/shared/components/ol/ol-tooltip' -import { - getBackgroundColorForUserId, - hslStringToLuminance, -} from '@/shared/utils/colors' -import classNames from 'classnames' -import { useCallback, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { Doc } from '@ol-types/doc' - -// Should be kept in sync with $max-user-circles-displayed CSS constant -const MAX_USER_CIRCLES_DISPLAYED = 5 - -// We don't want a +1 circle since we could just show the user instead -const MAX_USERS_WITH_OVERFLOW_VISIBLE = MAX_USER_CIRCLES_DISPLAYED - 1 - -export const OnlineUsersWidget = ({ - onlineUsers, - goToUser, -}: { - onlineUsers: OnlineUser[] - goToUser: (user: OnlineUser) => Promise -}) => { - const hasOverflow = onlineUsers.length > MAX_USER_CIRCLES_DISPLAYED - const usersBeforeOverflow = useMemo( - () => - hasOverflow - ? onlineUsers.slice(0, MAX_USERS_WITH_OVERFLOW_VISIBLE) - : onlineUsers, - [onlineUsers, hasOverflow] - ) - const usersInOverflow = useMemo( - () => - hasOverflow ? onlineUsers.slice(MAX_USERS_WITH_OVERFLOW_VISIBLE) : [], - [onlineUsers, hasOverflow] - ) - - return ( -
    - {usersBeforeOverflow.map((user, index) => ( - - ))} - {hasOverflow && ( - - )} -
    - ) -} - -const OnlineUserWidget = ({ - user, - goToUser, - id, -}: { - user: OnlineUser - goToUser: (user: OnlineUser) => void - id: string -}) => { - const onClick = useCallback(() => { - goToUser(user) - }, [goToUser, user]) - return ( - - - - ) -} - -const OnlineUserCircle = ({ user }: { user: OnlineUser }) => { - const backgroundColor = getBackgroundColorForUserId(user.user_id) - const luminance = hslStringToLuminance(backgroundColor) - const [character] = [...user.name] - return ( - = 0.5, - })} - style={{ backgroundColor }} - > - {character} - - ) -} - -const OnlineUserOverflow = ({ - goToUser, - users, -}: { - goToUser: (user: OnlineUser) => void - users: OnlineUser[] -}) => { - const { t } = useTranslation() - return ( - - - - +{users.length} - - - - - {users.map((user, index) => ( -
  • - goToUser(user)} - > - {user.name} - -
  • - ))} -
    -
    - ) -} diff --git a/services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx b/services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx index a05fb4eff8..50f519198a 100644 --- a/services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx +++ b/services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx @@ -5,7 +5,7 @@ export const MenuBar: FC< React.PropsWithChildren & { id: string }> > = ({ children, id, ...props }) => { return ( -
    +
    {children} diff --git a/services/web/frontend/stories/editor-navigation-toolbar.stories.jsx b/services/web/frontend/stories/editor-navigation-toolbar.stories.jsx deleted file mode 100644 index 8e3b619440..0000000000 --- a/services/web/frontend/stories/editor-navigation-toolbar.stories.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import ToolbarHeader from '../js/features/editor-navigation-toolbar/components/toolbar-header' -import { ScopeDecorator } from './decorators/scope' - -export const UpToThreeConnectedUsers = args => { - return -} -UpToThreeConnectedUsers.args = { - onlineUsers: ['a', 'c', 'd'].map(c => ({ - user_id: c, - name: `${c}_user name`, - })), -} - -export const ManyConnectedUsers = args => { - return -} -ManyConnectedUsers.args = { - onlineUsers: ['a', 'c', 'd', 'e', 'f'].map(c => ({ - user_id: c, - name: `${c}_user name`, - })), -} - -export default { - title: 'Editor / Toolbar', - component: ToolbarHeader, - argTypes: { - goToUser: { action: 'goToUser' }, - renameProject: { action: 'renameProject' }, - toggleHistoryOpen: { action: 'toggleHistoryOpen' }, - toggleReviewPanelOpen: { action: 'toggleReviewPanelOpen' }, - toggleChatOpen: { action: 'toggleChatOpen' }, - openShareModal: { action: 'openShareModal' }, - onShowLeftMenuClick: { action: 'onShowLeftMenuClick' }, - }, - args: { - projectName: 'Overleaf Project', - onlineUsers: [{ user_id: 'abc', name: 'overleaf' }], - unreadMessageCount: 0, - }, - decorators: [ScopeDecorator], -} diff --git a/services/web/frontend/stories/editor/layout-dropdown-button.stories.tsx b/services/web/frontend/stories/editor/layout-dropdown-button.stories.tsx deleted file mode 100644 index 93d68d6efb..0000000000 --- a/services/web/frontend/stories/editor/layout-dropdown-button.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { LayoutDropdownButtonUi } from '@/features/editor-navigation-toolbar/components/layout-dropdown-button' -import { Meta } from '@storybook/react' -import { ComponentProps } from 'react' - -export const LayoutDropdown = ( - props: ComponentProps -) => ( -
    -
    - -
    -
    -) - -const meta: Meta = { - title: 'Editor / Toolbar / Layout Dropdown', - component: LayoutDropdownButtonUi, - argTypes: { - view: { - control: 'select', - options: [null, 'editor', 'file', 'pdf', 'history'], - }, - detachRole: { - control: 'select', - options: ['detacher', 'detached'], - }, - pdfLayout: { - control: 'select', - options: ['sideBySide', 'flat'], - }, - }, - parameters: { actions: { argTypesRegex: '^handle.*' } }, -} - -export default meta diff --git a/services/web/frontend/stories/online-users.stories.tsx b/services/web/frontend/stories/online-users.stories.tsx index 6816035de4..bdbf761c50 100644 --- a/services/web/frontend/stories/online-users.stories.tsx +++ b/services/web/frontend/stories/online-users.stories.tsx @@ -1,7 +1,6 @@ import { Meta } from '@storybook/react' import { OnlineUser } from '@/features/ide-react/context/online-users-context' -import OnlineUsersWidgetOld from '@/features/editor-navigation-toolbar/components/online-users-widget' -import { OnlineUsersWidget } from '@/features/ide-redesign/components/online-users/online-users-widget' +import { OnlineUsersWidget } from '@/features/editor-navigation-toolbar/components/online-users-widget' const NAMES = [ 'Alice', @@ -31,7 +30,7 @@ const generateUser = (_: any, index: number): OnlineUser => { } } -export const OnlineUsersRedesign = ({ users }: { users: number }) => { +export const OnlineUsers = ({ users }: { users: number }) => { const generatedUsers = Array.from({ length: users }, generateUser) return (
    { ) } -export const OnlineUsersOld = ({ users }: { users: number }) => { - const generatedUsers = Array.from({ length: users }, generateUser) - return ( -
    - {}) as any} - /> -
    - ) -} - -const meta: Meta = { +const meta: Meta = { title: 'Editor / Online Users Widget', args: { users: 6, diff --git a/services/web/frontend/stylesheets/pages/editor/online-users.scss b/services/web/frontend/stylesheets/pages/editor/online-users.scss index 313f85f0ca..f9977cdfb0 100644 --- a/services/web/frontend/stylesheets/pages/editor/online-users.scss +++ b/services/web/frontend/stylesheets/pages/editor/online-users.scss @@ -1,47 +1,15 @@ :root { - --toolbar-btn-color: var(--white); --online-users-border-color: var(--bg-dark-primary); --online-users-text-color: var(--content-primary); --online-users-overflow-button-background-color: var(--bg-light-secondary); } @include theme('light') { - --toolbar-btn-color: var(--neutral-70); --online-users-border-color: var(--bg-light-primary); --online-users-text-color: var(--content-primary); --online-users-overflow-button-background-color: var(--bg-light-secondary); } -.online-users { - display: flex; - align-items: center; - - .online-user { - display: inline-block; - width: 24px; - height: 24px; - line-height: 24px; - margin-right: var(--spacing-04); - text-align: center; - color: white; - text-transform: uppercase; - border-radius: var(--border-radius-base); - cursor: pointer; - } - - .online-user-multi { - @include reset-button; - - color: var(--toolbar-btn-color); - width: auto; - min-width: 24px; - padding-left: var(--spacing-04); - padding-right: var(--spacing-03); - display: flex; - align-items: center; - } -} - .online-users-row { // Keep in sync with the MAX_USER_CIRCLES_DISPLAYED js constant $max-user-circles-displayed: 5; diff --git a/services/web/frontend/stylesheets/pages/editor/toolbar.scss b/services/web/frontend/stylesheets/pages/editor/toolbar.scss index a818888082..48305dab87 100644 --- a/services/web/frontend/stylesheets/pages/editor/toolbar.scss +++ b/services/web/frontend/stylesheets/pages/editor/toolbar.scss @@ -107,7 +107,7 @@ .toolbar-right, .toolbar-left { - button:not(.back-to-editor-btn, .toolbar-experiment-button) { + button:not(.back-to-editor-btn) { background: transparent; box-shadow: none; } @@ -133,7 +133,7 @@ .toolbar-left > a:not(.btn), .toolbar-left > button, .toolbar-right > a:not(.btn), - .toolbar-right > button:not(.back-to-editor-btn, .toolbar-experiment-button) { + .toolbar-right > button:not(.back-to-editor-btn) { display: inline-block; color: var(--toolbar-btn-color); background-color: transparent; @@ -198,11 +198,6 @@ right: var(--spacing-02); } - &.header-cobranding-logo-container { - height: calc(var(--toolbar-height) - 1px); - padding: var(--spacing-04) var(--spacing-05); - } - .spinner-border { vertical-align: middle; font-size: var(--font-size-02); @@ -263,34 +258,12 @@ display: flex; justify-content: center; } - - .toolbar-cio-tooltip { - position: absolute; - top: var(--toolbar-height); - width: 391px; - } - - #toolbar-cio-history { - @include media-breakpoint-down(lg) { - right: 10px; - - /* The iframe a message renders in isn't aware of the page width, so we - make it 1px narrower to enable max-width media queries */ - width: 390px; - } - } } .layout-dropdown .dropdown-item { align-items: center; } -.header-cobranding-logo { - display: block; - width: auto; - max-height: 100%; -} - .toolbar-label { display: none; margin: 0 var(--spacing-02); @@ -308,14 +281,6 @@ } } -.toolbar-header-upgrade-prompt { - margin-left: var(--spacing-05); - - @include media-breakpoint-down(lg) { - display: none !important; - } -} - .toolbar-filetree { @include toolbar-sm-height; @@ -324,57 +289,6 @@ flex-shrink: 0; } -.editor-menu-icon { - &.material-symbols { - width: 1em; - text-indent: -9999px; - background: var(--editor-header-logo-background); - } -} - -.project-name { - .name { - display: inline-block; - - @include text-truncate; - - padding: var(--spacing-03); - vertical-align: top; - color: var(--project-name-color); - font-weight: 700; - } - - input { - height: 30px; - margin-top: var(--spacing-02); - padding: var(--spacing-03); - max-width: 500px; - text-align: center; - font-weight: 700; - } - - a.rename { - visibility: hidden; - display: inline-block; - color: var(--project-rename-link-color); - padding: var(--spacing-03); - border-radius: var(--border-radius-base); - cursor: pointer; - - &:hover { - text-shadow: 0 1px 0 rgb(0 0 0 / 25%); - color: var(--project-rename-link-color-hover); - text-decoration: none; - } - } - - &:hover { - a.rename { - visibility: visible; - } - } -} - .toolbar-editor { height: var(--toolbar-small-height); background-color: var(--editor-toolbar-bg); @@ -531,16 +445,6 @@ border-right: 1px solid var(--formatting-btn-border); } -// Override a secondary button to ensure that the border is visible because -// overriding a borderless button will not add a border. -.toolbar-experiment-button.btn-secondary { - @include editor-switcher-button; - - max-height: 39px; - font-size: var(--font-size-01); - margin-right: var(--spacing-04); -} - .try-new-editor-button { .button-content { gap: var(--spacing-02); diff --git a/services/web/locales/en.json b/services/web/locales/en.json index b49d115b18..7cd736018a 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -672,14 +672,12 @@ "editing_captions": "Editing captions", "editing_tools": "Editing tools", "editor": "Editor", - "editor_and_pdf": "Editor & PDF", "editor_disconected_click_to_reconnect": "Editor disconnected, click anywhere to reconnect.", "editor_font_family": "Editor font family", "editor_font_size": "Editor font size", "editor_limit_exceeded_in_this_project": "Too many editors in this project", "editor_line_height": "Editor line height", "editor_only": "Editor only", - "editor_only_hide_pdf": "Editor only <0>(hide PDF)", "editor_theme": "Editor theme", "editor_theme_dark": "Dark editor theme", "editor_theme_light": "Light editor theme", @@ -1264,7 +1262,6 @@ "latex_templates_sentence_case": "LaTeX templates", "layout": "Layout", "layout_options": "Layout options", - "layout_processing": "Layout processing", "ldap": "LDAP", "ldap_create_admin_instructions": "Choose an email address for the first __appName__ admin account. This should correspond to an account in the LDAP system. You will then be asked to log in with this account.", "learn": "Learn", @@ -1691,9 +1688,7 @@ "pdf_compile_in_progress_error": "A previous compile is still running. Please wait a minute and try compiling again.", "pdf_compile_rate_limit_hit": "Compile rate limit hit", "pdf_compile_try_again": "Please wait for your other compile to finish before trying again.", - "pdf_in_separate_tab": "PDF in separate tab", "pdf_only": "PDF only", - "pdf_only_hide_editor": "PDF only <0>(hide editor)", "pdf_preview": "PDF preview", "pdf_preview_error": "There was a problem displaying the compilation results for this project.", "pdf_rendering_error": "PDF Rendering Error", @@ -1830,6 +1825,7 @@ "project_timed_out_intro": "Sorry, your compile took too long to run and timed out. The most common causes of timeouts are:", "project_timed_out_learn_more": "<0>Learn more about other causes of compile timeouts and how to fix them.", "project_timed_out_optimize_images": "Large or high-resolution images are taking too long to process. You may be able to <0>optimize them.", + "project_title_options": "Project title options", "project_too_large": "Project too large", "project_too_large_please_reduce": "This project has too much editable text, please try and reduce it. The largest files are:", "project_too_much_editable_text": "This project has too much editable text, please try to reduce it.", @@ -2342,7 +2338,6 @@ "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_design": "Switch to new editor design", - "switch_to_new_look": "Switch to new look", "switch_to_pdf": "Switch to PDF", "switch_to_standard_plan": "Switch to Standard plan", "symbol": "Symbol", @@ -2875,7 +2870,6 @@ "your_account_is_suspended": "Your account is suspended", "your_add_on_has_been_cancelled_and_will_remain_active_until_your_billing_cycle_ends_on": "Your add-on has been cancelled and will remain active until your billing cycle ends on __nextBillingDate__", "your_affiliation_is_confirmed": "Your <0>__institutionName__ affiliation is confirmed.", - "your_browser_does_not_support_this_feature": "Sorry, your browser doesn’t support this feature. Please update your browser to its latest version.", "your_changes_will_save": "Your changes will save when we get the connection back.", "your_compile_timed_out": "Your compile timed out", "your_current_plan": "Your current plan", diff --git a/services/web/test/frontend/features/editor-navigation-toolbar/components/chat-toggle-button.test.tsx b/services/web/test/frontend/features/editor-navigation-toolbar/components/chat-toggle-button.test.tsx deleted file mode 100644 index 76e408d28e..0000000000 --- a/services/web/test/frontend/features/editor-navigation-toolbar/components/chat-toggle-button.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from 'chai' -import { render, screen } from '@testing-library/react' - -import ChatToggleButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button' - -describe('', function () { - const defaultProps = { - chatIsOpen: false, - unreadMessageCount: 0, - onClick: () => {}, - } - - it('displays the number of unread messages', function () { - const props = { - ...defaultProps, - unreadMessageCount: 113, - } - render() - screen.getByText('113') - }) - - it("doesn't display the unread messages badge when the number of unread messages is zero", function () { - render() - expect(screen.queryByText('0')).to.not.exist - }) -}) diff --git a/services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.tsx b/services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.tsx deleted file mode 100644 index 5711827a5f..0000000000 --- a/services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import sinon from 'sinon' -import fetchMock from 'fetch-mock' -import { expect } from 'chai' -import { screen, waitFor } from '@testing-library/react' -import LayoutDropdownButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import * as eventTracking from '@/infrastructure/event-tracking' -import type { LayoutContextOwnStates } from '@/shared/context/layout-context' - -describe('', function () { - let openStub: sinon.SinonStub - let sendMBSpy: sinon.SinonSpy - - const defaultLayout: Partial = { - pdfLayout: 'flat', - view: 'pdf', - chatIsOpen: false, - } - - beforeEach(function () { - openStub = sinon.stub(window, 'open') - sendMBSpy = sinon.spy(eventTracking, 'sendMB') - }) - - afterEach(function () { - openStub.restore() - sendMBSpy.restore() - fetchMock.removeRoutes().clearHistory() - }) - - it('should mark current layout option as selected', async function () { - // Selected is aria-label, visually we show a checkmark - renderWithEditorContext(, { - layoutContext: defaultLayout, - }) - - screen.getByRole('button', { name: 'Layout' }).click() - - await waitFor(() => - expect( - screen - .getByRole('menuitem', { - name: 'Editor & PDF', - }) - .getAttribute('aria-selected') - ).to.equal('false') - ) - - expect( - screen - .getByRole('menuitem', { - name: 'PDF only (hide editor)', - }) - .getAttribute('aria-selected') - ).to.equal('true') - - expect( - screen - .getByRole('menuitem', { - name: 'Editor only (hide PDF)', - }) - .getAttribute('aria-selected') - ).to.equal('false') - - expect( - screen - .getByRole('menuitem', { - name: 'PDF in separate tab', - }) - .getAttribute('aria-selected') - ).to.equal('false') - }) - - it('should not select any option in history view', async function () { - // Selected is aria-label, visually we show a checkmark - renderWithEditorContext(, { - layoutContext: { ...defaultLayout, view: 'history' }, - }) - - screen.getByRole('button', { name: 'Layout' }).click() - - await waitFor(() => - expect( - screen - .getByRole('menuitem', { - name: 'Editor & PDF', - }) - .getAttribute('aria-selected') - ).to.equal('false') - ) - - expect( - screen - .getByRole('menuitem', { - name: 'PDF only (hide editor)', - }) - .getAttribute('aria-selected') - ).to.equal('false') - - expect( - screen - .getByRole('menuitem', { - name: 'Editor only (hide PDF)', - }) - .getAttribute('aria-selected') - ).to.equal('false') - - expect( - screen - .getByRole('menuitem', { - name: 'PDF in separate tab', - }) - .getAttribute('aria-selected') - ).to.equal('false') - }) - - it('should treat file and editor views the same way', async function () { - // Selected is aria-label, visually we show a checkmark - renderWithEditorContext(, { - layoutContext: { - pdfLayout: 'flat', - view: 'file', - chatIsOpen: false, - }, - }) - - screen.getByRole('button', { name: 'Layout' }).click() - - await waitFor(() => - expect( - screen - .getByRole('menuitem', { - name: 'Editor & PDF', - }) - .getAttribute('aria-selected') - ).to.equal('false') - ) - - expect( - screen - .getByRole('menuitem', { - name: 'PDF only (hide editor)', - }) - .getAttribute('aria-selected') - ).to.equal('false') - - expect( - screen - .getByRole('menuitem', { - name: 'Editor only (hide PDF)', - }) - .getAttribute('aria-selected') - ).to.equal('true') - - expect( - screen - .getByRole('menuitem', { - name: 'PDF in separate tab', - }) - .getAttribute('aria-selected') - ).to.equal('false') - }) - - describe('on detach', async function () { - const originalBroadcastChannel = window.BroadcastChannel - beforeEach(async function () { - // @ts-expect-error - window.BroadcastChannel = true // ensure that window.BroadcastChannel is truthy - - renderWithEditorContext(, { - layoutContext: { ...defaultLayout, view: 'editor' }, - }) - - screen.getByRole('button', { name: 'Layout' }).click() - - await waitFor(() => - screen - .getByRole('menuitem', { - name: 'PDF in separate tab', - }) - .click() - ) - }) - - afterEach(function () { - window.BroadcastChannel = originalBroadcastChannel - }) - - it('should show processing', async function () { - await screen.findByText('Layout processing') - }) - - it('should record event', function () { - sinon.assert.calledWith(sendMBSpy, 'project-layout-detach') - }) - }) - - describe('on layout change / reattach', async function () { - beforeEach(async function () { - window.metaAttributesCache.set('ol-detachRole', 'detacher') - renderWithEditorContext(, { - layoutContext: { ...defaultLayout, view: 'editor' }, - }) - - screen.getByRole('button', { name: 'Layout' }).click() - - await waitFor(() => - screen - .getByRole('menuitem', { - name: 'Editor only (hide PDF)', - }) - .click() - ) - }) - - it('should not show processing', function () { - const processingText = screen.queryByText('Layout processing') - expect(processingText).to.not.exist - }) - - it('should record events', function () { - sinon.assert.calledWith(sendMBSpy, 'project-layout-reattach') - sinon.assert.calledWith(sendMBSpy, 'project-layout-change', { - layout: 'flat', - view: 'editor', - page: '/detacher', - 'editor-redesign': 'enabled', - }) - }) - - it('should select new menu item', function () { - screen.getByRole('menuitem', { - name: 'Editor only (hide PDF)', - }) - }) - }) -}) diff --git a/services/web/test/frontend/features/editor-navigation-toolbar/components/online-users-widget.test.tsx b/services/web/test/frontend/features/editor-navigation-toolbar/components/online-users-widget.test.tsx index acdc82fa7a..377c70dc0e 100644 --- a/services/web/test/frontend/features/editor-navigation-toolbar/components/online-users-widget.test.tsx +++ b/services/web/test/frontend/features/editor-navigation-toolbar/components/online-users-widget.test.tsx @@ -1,115 +1,113 @@ import { expect } from 'chai' import sinon from 'sinon' import { fireEvent, render, screen } from '@testing-library/react' +import { OnlineUsersWidget } from '@/features/editor-navigation-toolbar/components/online-users-widget' -import OnlineUsersWidget from '../../../../../frontend/js/features/editor-navigation-toolbar/components/online-users-widget' +const names = ['alice', 'bob', 'charlie', 'dave', 'erin', 'frank', 'grace'] + +function makeUsers(count: number) { + return Array.from({ length: count }, (_, index) => ({ + id: `user_${index + 1}`, + user_id: `user_id_${index + 1}`, + name: names[index % names.length], + email: `${names[index % names.length]}_email`, + })) +} describe('', function () { const defaultProps = { - onlineUsers: [ - { - id: 'test_user', - user_id: 'test_user', - name: 'test_user', - email: 'test_email', - }, - { - id: 'another_test_user', - user_id: 'another_test_user', - name: 'another_test_user', - email: 'another_test_email', - }, - ], - goToUser: (async () => {}) as any, + onlineUsers: makeUsers(2), + goToUser: sinon.stub(), } - describe('with less than 4 users', function () { + describe('with less than 5 users', function () { it('displays user initials', function () { render() - screen.getByText('t') screen.getByText('a') + screen.getByText('b') }) it('displays user name in a tooltip', async function () { render() - const icon = screen.getByText('t') + const icon = screen.getByText('a') fireEvent.mouseOver(icon) - await screen.findByRole('tooltip', { name: 'test_user' }) + await screen.findByRole('tooltip', { name: 'alice' }) }) it('calls "goToUser" when the user initial is clicked', function () { - const props = { - ...defaultProps, - goToUser: sinon.stub(), - } - render() + render() - const icon = screen.getByText('t') + const icon = screen.getByText('a') fireEvent.click(icon) - expect(props.goToUser).to.be.calledWith({ - id: 'test_user', - user_id: 'test_user', - name: 'test_user', - email: 'test_email', + expect(defaultProps.goToUser).to.be.calledWith({ + id: 'user_1', + user_id: 'user_id_1', + name: 'alice', + email: 'alice_email', }) }) }) - describe('with 4 users and more', function () { + describe('with 5 users', function () { const props = { ...defaultProps, - onlineUsers: defaultProps.onlineUsers.concat([ - { - id: 'user_3', - user_id: 'user_3', - name: 'user_3', - email: 'user_3', - }, - { - id: 'user_4', - user_id: 'user_4', - name: 'user_4', - email: 'user_4', - }, - ]), + onlineUsers: makeUsers(5), } - it('displays the count of users', function () { + it('displays user initials', function () { render() - screen.getByText('4') + screen.getByText('a') + screen.getByText('b') + screen.getByText('c') + screen.getByText('d') + screen.getByText('e') + }) + }) + + describe('with more than 5 users', function () { + const props = { + ...defaultProps, + onlineUsers: makeUsers(7), + } + + it('displays a maximum of 4 user initials and an overflow icon', function () { + render() + screen.getByText('a') + screen.getByText('b') + screen.getByText('c') + screen.getByText('d') + screen.getByText('+3') }) - it('displays user names on hover', function () { + it('displays the remaining users in a dropdown when the overflow icon is clicked', async function () { render() + const overflowButton = screen.getByText('+3') + fireEvent.click(overflowButton) - const toggleButton = screen.getByRole('button') - fireEvent.click(toggleButton) - - screen.getByText('test_user') - screen.getByText('another_test_user') - screen.getByText('user_3') - screen.getByText('user_4') + await screen.findByText('erin') + await screen.findByText('frank') + await screen.findByText('grace') }) - it('calls "goToUser" when the user name is clicked', function () { - const testProps = { - ...props, - goToUser: sinon.stub(), + it('calls "goToUser" when a user in the overflow dropdown is clicked', async function () { + const props = { + ...defaultProps, + onlineUsers: makeUsers(7), } - render() - const toggleButton = screen.getByRole('button') - fireEvent.click(toggleButton) + render() + const overflowButton = screen.getByText('+3') + fireEvent.click(overflowButton) - const icon = screen.getByText('user_3') - fireEvent.click(icon) + const frankButton = await screen.findByText('frank') + fireEvent.click(frankButton) - expect(testProps.goToUser).to.be.calledWith({ - id: 'user_3', - user_id: 'user_3', - name: 'user_3', - email: 'user_3', + expect(props.goToUser).to.be.calledWith({ + id: 'user_6', + user_id: 'user_id_6', + name: 'frank', + email: 'frank_email', }) }) }) diff --git a/services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.tsx b/services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.tsx deleted file mode 100644 index cb7de63733..0000000000 --- a/services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { fireEvent, render, screen } from '@testing-library/react' - -import ProjectNameEditableLabel from '../../../../../frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label' - -describe('', function () { - const defaultProps = { projectName: 'test-project', onChange: () => {} } - - it('displays the project name', function () { - render() - screen.getByText('test-project') - }) - - describe('when the name is editable', function () { - const editableProps = { ...defaultProps, hasRenamePermissions: true } - - it('displays an editable input when the edit button is clicked', function () { - render() - fireEvent.click(screen.getByRole('button')) - screen.getByRole('textbox') - }) - - it('displays an editable input when the project name is double clicked', function () { - render() - fireEvent.doubleClick(screen.getByText('test-project')) - screen.getByRole('textbox') - }) - - it('calls "onChange" when the project name is updated', function () { - const props = { - ...editableProps, - onChange: sinon.stub(), - } - render() - - fireEvent.doubleClick(screen.getByText('test-project')) - const input = screen.getByRole('textbox') - - fireEvent.change(input, { target: { value: 'new project name' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - expect(props.onChange).to.be.calledWith('new project name') - }) - - it('calls "onChange" when the input loses focus', function () { - const props = { - ...editableProps, - onChange: sinon.stub(), - } - render() - - fireEvent.doubleClick(screen.getByText('test-project')) - const input = screen.getByRole('textbox') - - fireEvent.change(input, { target: { value: 'new project name' } }) - - fireEvent.blur(screen.getByRole('textbox')) - - expect(props.onChange).to.be.calledWith('new project name') - }) - }) - - describe('when the name is not editable', function () { - const nonEditableProps = { hasRenamePermissions: false, ...defaultProps } - - it('the edit button is not displayed', function () { - render() - expect(screen.queryByRole('button')).to.not.exist - }) - - it('does not display an editable input when the project name is double clicked', function () { - render() - fireEvent.doubleClick(screen.getByText('test-project')) - expect(screen.queryByRole('textbox')).to.not.exist - }) - }) -}) diff --git a/services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.tsx b/services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.tsx deleted file mode 100644 index e3dac691a5..0000000000 --- a/services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { expect } from 'chai' -import { screen } from '@testing-library/react' - -import ToolbarHeader, { - type ToolbarHeaderProps, -} from '../../../../../frontend/js/features/editor-navigation-toolbar/components/toolbar-header' -import { renderWithEditorContext } from '../../../helpers/render-with-context' - -describe('', function () { - const defaultProps: ToolbarHeaderProps = { - onShowLeftMenuClick: () => {}, - toggleChatOpen: () => {}, - toggleReviewPanelOpen: () => {}, - toggleHistoryOpen: () => {}, - unreadMessageCount: 0, - onlineUsers: [], - goToUser: (async () => {}) as any, - projectName: 'test project', - renameProject: () => {}, - openShareModal: () => {}, - hasPublishPermissions: true, - chatVisible: true, - trackChangesVisible: true, - cobranding: undefined, - isRestrictedTokenMember: false, - hasRenamePermissions: true, - historyIsOpen: false, - chatIsOpen: false, - reviewPanelOpen: false, - } - - beforeEach(function () { - window.metaAttributesCache.set('ol-preventCompileOnLoad', true) - }) - - describe('cobranding logo', function () { - it('is not displayed by default', function () { - renderWithEditorContext() - expect(screen.queryByRole('link', { name: 'variation' })).to.not.exist - }) - - it('is displayed when cobranding data is available', function () { - const props = { - ...defaultProps, - cobranding: { - brandId: 12, - brandVariationId: 12, - brandVariationHomeUrl: 'http://cobranding', - brandVariationName: 'variation', - logoImgUrl: 'http://cobranding/logo', - }, - } - renderWithEditorContext() - screen.getByRole('link', { name: 'variation' }) - }) - }) - - describe('track changes toggle button', function () { - it('is displayed by default', function () { - renderWithEditorContext() - screen.getByText('Review') - }) - - it('is not displayed when "trackChangesVisible" prop is set to false', function () { - const props = { - ...defaultProps, - trackChangesVisible: false, - } - renderWithEditorContext() - expect(screen.queryByText('Review')).to.not.exist - }) - }) - - describe('History toggle button', function () { - it('is displayed by default', function () { - renderWithEditorContext() - screen.getByText('History') - }) - - it('is not displayed when "isRestrictedTokenMember" prop is set to true', function () { - const props = { - ...defaultProps, - isRestrictedTokenMember: true, - } - renderWithEditorContext() - expect(screen.queryByText('History')).to.not.exist - }) - }) - - describe('Chat toggle button', function () { - it('is displayed by default', function () { - renderWithEditorContext() - screen.getByText('Chat') - }) - - it('is not displayed when "chatVisible" prop is set to false', function () { - const props = { - ...defaultProps, - chatVisible: false, - } - renderWithEditorContext() - expect(screen.queryByText('Chat')).to.not.exist - }) - }) - - describe('Publish button', function () { - it('is displayed by default', function () { - renderWithEditorContext() - screen.getByText('Submit') - }) - - it('is not displayed for users with no publish permissions', function () { - const props = { - ...defaultProps, - hasPublishPermissions: false, - } - renderWithEditorContext() - expect(screen.queryByText('Submit')).to.not.exist - }) - }) -}) diff --git a/services/web/test/frontend/features/ide-react/unit/cobranding-logo.png b/services/web/test/frontend/features/ide-react/unit/cobranding-logo.png new file mode 100644 index 0000000000..597339bafe Binary files /dev/null and b/services/web/test/frontend/features/ide-react/unit/cobranding-logo.png differ diff --git a/services/web/test/frontend/features/ide-react/unit/toolbar.spec.tsx b/services/web/test/frontend/features/ide-react/unit/toolbar.spec.tsx new file mode 100644 index 0000000000..502e06a377 --- /dev/null +++ b/services/web/test/frontend/features/ide-react/unit/toolbar.spec.tsx @@ -0,0 +1,248 @@ +import { Toolbar } from '@/features/ide-react/components/toolbar/toolbar' +import { + EditorProviders, + makeEditorProvider, +} from '../../../helpers/editor-providers' +import { Cobranding } from '@ol-types/cobranding' +import partnerLogoUrl from './cobranding-logo.png' + +describe('', function () { + describe('cobranding', function () { + beforeEach(function () { + const cobranding: Cobranding = { + logoImgUrl: partnerLogoUrl, + brandVariationName: 'brand variation name', + brandVariationId: 1000, + brandId: 2000, + brandVariationHomeUrl: 'https://brand.example.com', + publishGuideHtml: 'string', + partner: 'partner name', + brandedMenu: true, + submitBtnHtml: 'submit to\n partner', + submitBtnHtmlNoBreaks: 'submit to partner', + } + + cy.mount( + + + + ) + }) + + it('displays the cobranding logo', function () { + cy.get(`img[src="${partnerLogoUrl}"]`).should('be.visible') + }) + + it('shows the branded submit button', function () { + cy.findByRole('button', { name: 'submit to partner' }).should( + 'be.visible' + ) + }) + }) + + describe('no cobranding', function () { + beforeEach(function () { + cy.mount( + + + + ) + }) + + it('does not display a cobranding logo', function () { + cy.get('.ide-redesign-toolbar-cobranding-logo').should('not.exist') + }) + + it('does not show a submit button', function () { + cy.findByRole('button', { name: 'Submit' }).should('not.exist') + }) + }) + + it('clicking the logo takes you to the home page', function () { + cy.mount( + + + + ) + cy.findByLabelText('Overleaf logo') + // We can't click the link in component tests, so just look at the + // parent directly + .parent() + .should('be.visible') + .should('have.attr', 'href', '/project') + }) + + describe('project title menu', function () { + beforeEach(function () { + cy.mount( + + + + ) + }) + + it('displays the project title dropdown', function () { + cy.findByRole('button', { name: 'Project title options' }) + .should('be.visible') + .should('contain.text', 'My title') + .click() + + cy.findByRole('menu') + .should('exist') + .within(() => { + cy.findByRole('menuitem', { name: 'Download as PDF' }).should('exist') + cy.findByRole('menuitem', { + name: 'Download as source (.zip)', + }).should('exist') + cy.findByRole('menuitem', { name: 'Make a copy' }).should('exist') + cy.findByRole('menuitem', { name: 'Rename' }).should('exist') + }) + }) + + it('allows the project to be renamed', function () { + cy.findByRole('button', { name: 'Project title options' }).click() + cy.findByRole('menuitem', { name: 'Rename' }).click() + cy.findByRole('textbox') + .should('be.visible') + .should('have.value', 'My title') + .type('New title{enter}') + cy.get('@rename-project').should('have.been.calledWith', 'New title') + }) + + it('should show modal when copying project', function () { + cy.findByRole('button', { name: 'Project title options' }).click() + cy.findByRole('menuitem', { name: 'Make a copy' }).click() + cy.findByRole('dialog') + .should('be.visible') + .should('contain.text', 'Copy project') + }) + + it('should show modal when pressing submit', function () { + cy.findByRole('button', { name: 'Project title options' }).click() + cy.findByRole('menuitem', { name: 'Submit' }).click() + cy.findByRole('dialog') + .should('be.visible') + .should('contain.text', 'Submit') + }) + }) + + describe('history toggle', function () { + it('Should show history button', function () { + cy.mount( + + + + ) + cy.findByRole('button', { name: 'History' }).should('be.visible').click() + }) + + it('Should not show history button to restricted token members', function () { + cy.mount( + + + + ) + cy.findByRole('button', { name: 'History' }).should('not.exist') + }) + }) + + it('should show share button', function () { + cy.mount( + + + + ) + cy.findByRole('button', { name: 'Share' }).should('be.visible').click() + cy.findByRole('dialog') + .should('be.visible') + .should('contain.text', 'Share Project') + }) + + it('should show layout button', function () { + cy.mount( + + + + ) + cy.findByRole('button', { name: 'Layout options' }) + .should('be.visible') + .click() + cy.findByRole('menu') + .should('exist') + .within(() => { + cy.findByRole('menuitem', { name: 'Split view' }).should('exist') + cy.findByRole('menuitem', { name: 'Editor only' }).should('exist') + cy.findByRole('menuitem', { name: 'PDF only' }).should('exist') + cy.findByRole('menuitem', { name: 'Open PDF in separate tab' }).should( + 'exist' + ) + }) + }) + + describe('for non-owner', function () { + it('should disable rename option', function () { + cy.mount( + + + + ) + cy.findByRole('button', { name: 'Project title options' }).click() + // FIXME: This should really be a button rather than a link + cy.findByRole('menuitem', { name: 'Rename' }).should( + 'have.class', + 'disabled' + ) + }) + }) + + describe('menu bar', function () { + beforeEach(function () { + cy.mount( + + + + ) + }) + + it('should have a menu bar role', function () { + cy.findByRole('menubar').should('be.visible') + }) + + it('should show file, view & help', function () { + cy.findByRole('menubar').within(() => { + cy.findByRole('button', { name: 'File' }).should('be.visible') + cy.findByRole('button', { name: 'View' }).should('be.visible') + cy.findByRole('button', { name: 'Help' }).should('be.visible') + }) + }) + + // TODO: Test all the dynamic items + }) +}) diff --git a/services/web/test/frontend/helpers/editor-providers.tsx b/services/web/test/frontend/helpers/editor-providers.tsx index 6ec6a3b872..2be7622403 100644 --- a/services/web/test/frontend/helpers/editor-providers.tsx +++ b/services/web/test/frontend/helpers/editor-providers.tsx @@ -54,6 +54,7 @@ import { UserSettings } from '@ol-types/user-settings' import { DetachCompileContext } from '@/shared/context/detach-compile-context' import { type CompileContext } from '@/shared/context/local-compile-context' import { EditorContext } from '@/shared/context/editor-context' +import { Cobranding } from '@ol-types/cobranding' // these constants can be imported in tests instead of // using magic strings @@ -267,11 +268,21 @@ export function EditorProviders({ ) } -export function makeEditorProvider({ isProjectOwner = true } = {}) { +export function makeEditorProvider({ + isProjectOwner = true, + cobranding = undefined, + renameProject = () => {}, + isRestrictedTokenMember, +}: { + isProjectOwner?: boolean + cobranding?: Cobranding + renameProject?: () => void + isRestrictedTokenMember?: boolean +} = {}) { const EditorProvider: FC = ({ children }) => { const value = { isProjectOwner, - renameProject: () => {}, + renameProject, isPendingEditor: false, deactivateTutorial: () => {}, inactiveTutorials: [], @@ -285,6 +296,8 @@ export function makeEditorProvider({ isProjectOwner = true } = {}) { setWritefullInstance: () => {}, showUpgradeModal: false, setShowUpgradeModal: () => {}, + cobranding, + isRestrictedTokenMember, } return (