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 (
-
-
-
- )
-}
-
-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 (
-
-
-
- )
-}
-
-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 (
-
-
-
-
- {t('connected_users')}
-
- {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}
+
+
+
+
+ {t('connected_users')}
+
+ {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}
-
-
-
-
- {t('connected_users')}
-
- {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)0>",
"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)0>",
"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 more0> 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 them0>.",
+ "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__0> 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 (