Просмотр исходного кода

Merge pull request #31397 from overleaf/mj-old-editor-toolbar-teardown

[web] Tear down old editor toolbar

GitOrigin-RevId: 8ba74abcc56e7bd476a9d6cae72f38486168c2ed
Mathias Jakobsen 6 месяцев назад
Родитель
Сommit
cc1dedca7d
32 измененных файлов с 454 добавлено и 1787 удалено
  1. 0 8
      services/web/frontend/extracted-translations.json
  2. 0 35
      services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.tsx
  3. 0 36
      services/web/frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button.tsx
  4. 0 26
      services/web/frontend/js/features/editor-navigation-toolbar/components/cobranding-logo.tsx
  5. 0 18
      services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.tsx
  6. 0 241
      services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.tsx
  7. 0 17
      services/web/frontend/js/features/editor-navigation-toolbar/components/menu-button.tsx
  8. 114 99
      services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.tsx
  9. 0 99
      services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.tsx
  10. 0 18
      services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.tsx
  11. 0 162
      services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx
  12. 0 36
      services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.tsx
  13. 0 27
      services/web/frontend/js/features/editor-navigation-toolbar/components/upgrade-prompt.tsx
  14. 0 45
      services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx
  15. 1 1
      services/web/frontend/js/features/ide-react/components/toolbar/online-users.tsx
  16. 2 4
      services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx
  17. 0 146
      services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx
  18. 1 1
      services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx
  19. 0 42
      services/web/frontend/stories/editor-navigation-toolbar.stories.jsx
  20. 0 35
      services/web/frontend/stories/editor/layout-dropdown-button.stories.tsx
  21. 3 21
      services/web/frontend/stories/online-users.stories.tsx
  22. 0 32
      services/web/frontend/stylesheets/pages/editor/online-users.scss
  23. 2 98
      services/web/frontend/stylesheets/pages/editor/toolbar.scss
  24. 1 7
      services/web/locales/en.json
  25. 0 26
      services/web/test/frontend/features/editor-navigation-toolbar/components/chat-toggle-button.test.tsx
  26. 0 237
      services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.tsx
  27. 67 69
      services/web/test/frontend/features/editor-navigation-toolbar/components/online-users-widget.test.tsx
  28. 0 78
      services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.tsx
  29. 0 121
      services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.tsx
  30. BIN
      services/web/test/frontend/features/ide-react/unit/cobranding-logo.png
  31. 248 0
      services/web/test/frontend/features/ide-react/unit/toolbar.spec.tsx
  32. 15 2
      services/web/test/frontend/helpers/editor-providers.tsx

+ 0 - 8
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": "",

+ 0 - 35
services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.tsx

@@ -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 (
-    <OLTooltip
-      id="back-to-projects"
-      description={t('back_to_your_projects')}
-      overlayProps={{ placement: 'right' }}
-    >
-      <div className="toolbar-item">
-        <a
-          className="btn btn-full-height"
-          draggable="false"
-          href="/project"
-          onClick={() => {
-            eventTracking.sendMB('navigation-clicked-home')
-          }}
-        >
-          <MaterialIcon
-            type="home"
-            className="align-text-bottom"
-            accessibilityLabel={t('back_to_your_projects')}
-          />
-        </a>
-      </div>
-    </OLTooltip>
-  )
-}
-
-export default BackToProjectsButton

+ 0 - 36
services/web/frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button.tsx

@@ -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 (
-    <div className="toolbar-item">
-      <button type="button" className={classes} onClick={onClick}>
-        <MaterialIcon
-          type="chat"
-          className={classNames('align-middle', {
-            bounce: hasUnreadMessages,
-          })}
-        />
-        {hasUnreadMessages && <OLBadge bg="info">{unreadMessageCount}</OLBadge>}
-        <p className="toolbar-label">{t('chat')}</p>
-      </button>
-    </div>
-  )
-}
-
-export default ChatToggleButton

+ 0 - 26
services/web/frontend/js/features/editor-navigation-toolbar/components/cobranding-logo.tsx

@@ -1,26 +0,0 @@
-function CobrandingLogo({
-  brandVariationHomeUrl,
-  brandVariationName,
-  logoImgUrl,
-}: {
-  brandVariationHomeUrl: string
-  brandVariationName: string
-  logoImgUrl: string
-}) {
-  return (
-    <a
-      className="btn btn-full-height header-cobranding-logo-container"
-      href={brandVariationHomeUrl}
-      target="_blank"
-      rel="noreferrer noopener"
-    >
-      <img
-        src={logoImgUrl}
-        className="header-cobranding-logo"
-        alt={brandVariationName}
-      />
-    </a>
-  )
-}
-
-export default CobrandingLogo

+ 0 - 18
services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.tsx

@@ -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 (
-    <div className="toolbar-item">
-      <button type="button" className="btn btn-full-height" onClick={onClick}>
-        <MaterialIcon type="history" className="align-middle" />
-        <p className="toolbar-label">{t('history')}</p>
-      </button>
-      <div id="toolbar-cio-history" className="toolbar-cio-tooltip" />
-    </div>
-  )
-}
-
-export default HistoryToggleButton

+ 0 - 241
services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.tsx

@@ -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<typeof DropdownItem>) {
-  return (
-    <DropdownItem
-      active={active}
-      aria-current={active}
-      trailingIcon={active ? 'check' : null}
-      {...props}
-    />
-  )
-}
-
-const LayoutDropdownToggleButton = forwardRef<
-  HTMLButtonElement,
-  {
-    onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
-  }
->(({ onClick, ...props }, ref) => {
-  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
-    eventTracking.sendMB('navigation-clicked-layout')
-    onClick(e)
-  }
-
-  return <DropdownToggleCustom {...props} ref={ref} onClick={handleClick} />
-})
-LayoutDropdownToggleButton.displayName = 'LayoutDropdownToggleButton'
-
-function BS5DetachDisabled() {
-  const { t } = useTranslation()
-
-  return (
-    <OLTooltip
-      id="detach-disabled"
-      description={t('your_browser_does_not_support_this_feature')}
-      overlayProps={{ placement: 'left' }}
-    >
-      <span>
-        <EnhancedDropdownItem disabled leadingIcon="select_window">
-          {t('pdf_in_separate_tab')}
-        </EnhancedDropdownItem>
-      </span>
-    </OLTooltip>
-  )
-}
-
-function LayoutDropdownButton() {
-  const {
-    detachIsLinked,
-    detachRole,
-    view,
-    pdfLayout,
-    handleChangeLayout,
-    handleDetach,
-  } = useLayoutContext()
-
-  return (
-    <LayoutDropdownButtonUi
-      processing={!detachIsLinked && detachRole === 'detacher'}
-      handleChangeLayout={handleChangeLayout}
-      handleDetach={handleDetach}
-      detachIsLinked={detachIsLinked}
-      detachRole={detachRole}
-      pdfLayout={pdfLayout}
-      view={view}
-      detachable={'BroadcastChannel' in window}
-    />
-  )
-}
-
-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 (
-    <>
-      <div aria-live="assertive" className="visually-hidden" id="layout-status">
-        {processing ? t('layout_processing') : ''}
-      </div>
-      <Dropdown className="toolbar-item layout-dropdown" align="end">
-        <DropdownToggle
-          aria-describedby={processing ? 'layout-status' : undefined}
-          id="layout-dropdown-btn"
-          className="btn-full-height"
-          as={LayoutDropdownToggleButton}
-        >
-          {processing ? (
-            <OLSpinner size="sm" />
-          ) : (
-            <MaterialIcon type="dock_to_right" className="align-middle" />
-          )}
-          <span className="toolbar-label">{t('layout')}</span>
-        </DropdownToggle>
-        <DropdownMenu>
-          <EnhancedDropdownItem
-            onClick={() => handleChangeLayout('sideBySide')}
-            active={isActiveDropdownItem({
-              iconFor: 'sideBySide',
-              pdfLayout,
-              view,
-              detachRole,
-            })}
-            leadingIcon="dock_to_right"
-          >
-            {t('editor_and_pdf')}
-          </EnhancedDropdownItem>
-
-          <EnhancedDropdownItem
-            onClick={() => handleChangeLayout('flat', 'editor')}
-            active={isActiveDropdownItem({
-              iconFor: 'editorOnly',
-              pdfLayout,
-              view,
-              detachRole,
-            })}
-            leadingIcon="code"
-          >
-            <div className="d-flex flex-column">
-              <Trans
-                i18nKey="editor_only_hide_pdf"
-                components={[
-                  <span key="editor_only_hide_pdf" className="subdued" />,
-                ]}
-              />
-            </div>
-          </EnhancedDropdownItem>
-
-          <EnhancedDropdownItem
-            onClick={() => handleChangeLayout('flat', 'pdf')}
-            active={isActiveDropdownItem({
-              iconFor: 'pdfOnly',
-              pdfLayout,
-              view,
-              detachRole,
-            })}
-            leadingIcon="picture_as_pdf"
-          >
-            <div className="d-flex flex-column">
-              <Trans
-                i18nKey="pdf_only_hide_editor"
-                components={[
-                  <span key="pdf_only_hide_editor" className="subdued" />,
-                ]}
-              />
-            </div>
-          </EnhancedDropdownItem>
-
-          {detachable ? (
-            <EnhancedDropdownItem
-              onClick={() => handleDetach()}
-              active={detachRole === 'detacher' && detachIsLinked}
-              trailingIcon={
-                detachRole === 'detacher' ? (
-                  detachIsLinked ? (
-                    'check'
-                  ) : (
-                    <OLSpinner size="sm" />
-                  )
-                ) : null
-              }
-              leadingIcon="select_window"
-            >
-              {t('pdf_in_separate_tab')}
-            </EnhancedDropdownItem>
-          ) : (
-            <BS5DetachDisabled />
-          )}
-        </DropdownMenu>
-      </Dropdown>
-    </>
-  )
-}
-
-export default memo(LayoutDropdownButton)

+ 0 - 17
services/web/frontend/js/features/editor-navigation-toolbar/components/menu-button.tsx

@@ -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 (
-    <div className="toolbar-item">
-      <button type="button" className="btn btn-full-height" onClick={onClick}>
-        <MaterialIcon type="menu" className="editor-menu-icon align-middle" />
-        <p className="toolbar-label">{t('menu')}</p>
-      </button>
-    </div>
-  )
-}
-
-export default MenuButton

+ 114 - 99
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<Doc | undefined>
-}) {
-  const { t } = useTranslation()
-
-  const shouldDisplayDropdown = onlineUsers.length >= 4
+}) => {
+  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]
+  )
 
-  if (shouldDisplayDropdown) {
-    return (
-      <Dropdown className="online-users" align="end">
-        <DropdownToggle
-          id="online-users"
-          as={DropDownToggleButton}
-          // @ts-ignore: fix type of DropdownToggle with "as" prop so that it can accept
-          // custom props for that component
-          onlineUserCount={onlineUsers.length}
+  return (
+    <div className="online-users-row">
+      {usersBeforeOverflow.map((user, index) => (
+        <OnlineUserWidget
+          key={`${user.user_id}_${index}`}
+          user={user}
+          goToUser={goToUser}
+          id={`online-user-${user.user_id}_${index}`}
         />
-        <DropdownMenu>
-          <DropdownHeader aria-hidden="true">
-            {t('connected_users')}
-          </DropdownHeader>
-          {onlineUsers.map((user, index) => (
-            <li role="none" key={`${user.user_id}_${index}`}>
-              <DropdownItem
-                as="button"
-                tabIndex={-1}
-                onClick={() => goToUser(user)}
-              >
-                <UserIcon user={user} showName />
-              </DropdownItem>
-            </li>
-          ))}
-        </DropdownMenu>
-      </Dropdown>
-    )
-  } else {
-    return (
-      <div className="online-users">
-        {onlineUsers.map((user, index) => (
-          <OLTooltip
-            key={`${user.user_id}_${index}`}
-            id="online-user"
-            description={user.name}
-            overlayProps={{ placement: 'bottom', trigger: ['hover', 'focus'] }}
-            tooltipProps={{ translate: 'no' }}
-          >
-            <span>
-              {/* OverlayTrigger won't fire unless UserIcon is wrapped in a span */}
-              <UserIcon user={user} onClick={goToUser} />
-            </span>
-          </OLTooltip>
-        ))}
-      </div>
-    )
-  }
+      ))}
+      {hasOverflow && (
+        <OnlineUserOverflow goToUser={goToUser} users={usersInOverflow} />
+      )}
+    </div>
+  )
 }
 
-function UserIcon({
+const OnlineUserWidget = ({
   user,
-  showName,
-  onClick,
+  goToUser,
+  id,
 }: {
   user: OnlineUser
-  showName?: boolean
-  onClick?: (user: OnlineUser) => void
-}) {
-  const backgroundColor = getBackgroundColorForUserId(user.user_id)
-
-  function handleOnClick() {
-    onClick?.(user)
-  }
+  goToUser: (user: OnlineUser) => void
+  id: string
+}) => {
+  const onClick = useCallback(() => {
+    goToUser(user)
+  }, [goToUser, user])
+  return (
+    <OLTooltip
+      id={id}
+      description={user.name}
+      overlayProps={{
+        placement: 'bottom',
+        trigger: ['hover', 'focus'],
+        delay: 0,
+      }}
+    >
+      <button className="online-users-row-button" onClick={onClick}>
+        <OnlineUserCircle user={user} />
+      </button>
+    </OLTooltip>
+  )
+}
 
+const OnlineUserCircle = ({ user }: { user: OnlineUser }) => {
+  const backgroundColor = getBackgroundColorForUserId(user.user_id)
+  const luminance = hslStringToLuminance(backgroundColor)
   const [character] = [...user.name]
-
   return (
-    // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-    <span onClick={handleOnClick}>
-      <span className="online-user" style={{ backgroundColor }}>
-        {character}
-      </span>
-      {showName && user.name}
+    <span
+      className={classNames('online-user-circle', {
+        'online-user-circle-light-font': luminance < 0.5,
+        'online-user-circle-dark-font': luminance >= 0.5,
+      })}
+      style={{ backgroundColor }}
+    >
+      {character}
     </span>
   )
 }
 
-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 (
-    <OLTooltip
-      id="connected-users"
-      description={t('connected_users')}
-      overlayProps={{ placement: 'left' }}
-    >
-      <button
-        type="button"
-        className="online-user online-user-multi"
-        onClick={props.onClick} // required by Bootstrap Dropdown to trigger an opening
-        ref={ref}
-      >
-        <strong>{props.onlineUserCount}</strong>&nbsp;
-        <MaterialIcon type="groups" />
-      </button>
-    </OLTooltip>
+    <Dropdown align="end">
+      <DropdownToggle className="online-users-row-button online-user-overflow-toggle">
+        <OLTooltip
+          id="connected-users"
+          description={t('n_more_collaborators', { count: users.length })}
+          overlayProps={{ placement: 'bottom' }}
+        >
+          <span className="online-user-circle">+{users.length}</span>
+        </OLTooltip>
+      </DropdownToggle>
+      <DropdownMenu className="online-user-overflow-dropdown">
+        <DropdownHeader aria-hidden="true">
+          {t('connected_users')}
+        </DropdownHeader>
+        {users.map((user, index) => (
+          <li role="none" key={`${user.user_id}_${index}`}>
+            <DropdownItem
+              as="button"
+              tabIndex={-1}
+              onClick={() => goToUser(user)}
+            >
+              <OnlineUserCircle user={user} /> {user.name}
+            </DropdownItem>
+          </li>
+        ))}
+      </DropdownMenu>
+    </Dropdown>
   )
-})
-
-DropDownToggleButton.displayName = 'DropDownToggleButton'
-
-export default OnlineUsersWidget
+}

+ 0 - 99
services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.tsx

@@ -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<HTMLInputElement | null>(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<HTMLInputElement>) {
-    setInputContent(event.target.value)
-  }
-
-  function handleBlur() {
-    if (isRenaming) {
-      finishRenaming()
-    }
-  }
-
-  return (
-    <div className={classNames('project-name', className)}>
-      {!isRenaming && (
-        <span className="name" onDoubleClick={startRenaming}>
-          {projectName}
-        </span>
-      )}
-      {isRenaming && (
-        <OLFormControl
-          ref={inputRef}
-          type="text"
-          onKeyDown={handleKeyDown}
-          onChange={handleOnChange}
-          onBlur={handleBlur}
-          value={inputContent}
-        />
-      )}
-      {canRename && (
-        <OLTooltip
-          id="online-user"
-          description={t('rename')}
-          overlayProps={{ placement: 'bottom', trigger: ['hover', 'focus'] }}
-        >
-          {/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */}
-          <a className="rename" role="button" onClick={startRenaming}>
-            <MaterialIcon type="edit" className="align-text-bottom" />
-          </a>
-        </OLTooltip>
-      )}
-    </div>
-  )
-}
-
-export default ProjectNameEditableLabel

+ 0 - 18
services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.tsx

@@ -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 (
-    <div className="toolbar-item">
-      <button type="button" className="btn btn-full-height" onClick={onClick}>
-        <MaterialIcon type="group_add" className="align-middle" />
-        <p className="toolbar-label">{t('share')}</p>
-      </button>
-      <div id="toolbar-cio-share" className="toolbar-cio-tooltip" />
-    </div>
-  )
-}
-
-export default ShareProjectButton

+ 0 - 162
services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx

@@ -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<Doc | undefined>
-  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 (
-    <nav className="toolbar toolbar-header" aria-label={t('project_actions')}>
-      <div className="toolbar-left">
-        <MenuButton onClick={onShowLeftMenuClick} />
-        {cobranding && cobranding.logoImgUrl && (
-          <CobrandingLogo {...cobranding} />
-        )}
-        <BackToProjectsButton />
-        {enableROMirrorOnClient &&
-          offlineModeToolbarButtons.map(
-            ({ path, import: { default: OfflineModeToolbarButton } }) => {
-              return <OfflineModeToolbarButton key={path} />
-            }
-          )}
-      </div>
-      {getMeta('ol-showUpgradePrompt') && (
-        <div className="d-flex align-items-center">
-          <UpgradePrompt />
-        </div>
-      )}
-      <ProjectNameEditableLabel
-        className="toolbar-center"
-        projectName={projectName}
-        hasRenamePermissions={hasRenamePermissions}
-        onChange={renameProject}
-      />
-
-      <div className="toolbar-right">
-        {canUseNewEditor() && <TryNewEditorButton />}
-
-        <OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
-
-        {historyIsOpen ? (
-          <div className="d-flex align-items-center">
-            <BackToEditorButton onClick={toggleHistoryOpen} />
-          </div>
-        ) : (
-          <>
-            {trackChangesVisible && (
-              <TrackChangesToggleButton
-                onMouseDown={toggleReviewPanelOpen}
-                disabled={historyIsOpen}
-                trackChangesIsOpen={reviewPanelOpen}
-              />
-            )}
-
-            <ShareProjectButton onClick={openShareModal} />
-            {shouldDisplayPublishButton && (
-              <PublishButton cobranding={cobranding} />
-            )}
-
-            {!isRestrictedTokenMember && (
-              <HistoryToggleButton onClick={toggleHistoryOpen} />
-            )}
-
-            <LayoutDropdownButton />
-
-            {chatEnabled && chatVisible && (
-              <ChatToggleButton
-                chatIsOpen={chatIsOpen}
-                onClick={toggleChatOpen}
-                unreadMessageCount={unreadMessageCount}
-              />
-            )}
-          </>
-        )}
-      </div>
-    </nav>
-  )
-})
-
-export default ToolbarHeader

+ 0 - 36
services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.tsx

@@ -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 (
-    <div className="toolbar-item">
-      <button
-        type="button"
-        disabled={disabled}
-        className={classes}
-        onMouseDown={onMouseDown}
-      >
-        <MaterialIcon type="rate_review" className="align-middle" />
-        <p className="toolbar-label">{t('review')}</p>
-      </button>
-      <div id="toolbar-cio-review" className="toolbar-cio-tooltip" />
-    </div>
-  )
-}
-
-export default TrackChangesToggleButton

+ 0 - 27
services/web/frontend/js/features/editor-navigation-toolbar/components/upgrade-prompt.tsx

@@ -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 (
-    <OLButton
-      variant="primary"
-      size="sm"
-      className="toolbar-header-upgrade-prompt"
-      href="/user/subscription/plans?itm_referrer=editor-header-upgrade-prompt"
-      target="_blank"
-      onClick={handleClick}
-    >
-      {t('upgrade')}
-    </OLButton>
-  )
-}
-
-export default UpgradePrompt

+ 0 - 45
services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx

@@ -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<HTMLButtonElement | null>(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 (
-    <div className="d-flex align-items-center">
-      <OLButton
-        className="toolbar-experiment-button try-new-editor-button"
-        onClick={onClick}
-        size="sm"
-        variant="secondary"
-        isLoading={loading}
-        ref={buttonRef}
-      >
-        <MaterialIcon type="fiber_new" />
-        {t('switch_to_new_look')}
-      </OLButton>
-      <OldEditorWarningTooltip target={buttonElt} />
-    </div>
-  )
-}
-
-export default TryNewEditorButton

+ 1 - 1
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()

+ 2 - 4
services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx

@@ -59,15 +59,13 @@ export const ToolbarProjectTitle = () => {
     <Dropdown align="start" className="ide-redesign-toolbar-project-dropdown">
       <DropdownToggle
         id="project-title-options"
+        aria-label={t('project_title_options')}
         className="ide-redesign-toolbar-project-dropdown-toggle ide-redesign-toolbar-dropdown-toggle-subdued fw-bold ide-redesign-toolbar-button-subdued"
       >
         <span className="ide-redesign-toolbar-project-name" translate="no">
           {name}
         </span>
-        <MaterialIcon
-          type="keyboard_arrow_down"
-          accessibilityLabel={t('project_title_options')}
-        />
+        <MaterialIcon type="keyboard_arrow_down" />
       </DropdownToggle>
       <DropdownMenu renderOnMount>
         {shouldDisplaySubmitButton && !cobranding && (

+ 0 - 146
services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx

@@ -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<Doc | undefined>
-}) => {
-  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 (
-    <div className="online-users-row">
-      {usersBeforeOverflow.map((user, index) => (
-        <OnlineUserWidget
-          key={`${user.user_id}_${index}`}
-          user={user}
-          goToUser={goToUser}
-          id={`online-user-${user.user_id}_${index}`}
-        />
-      ))}
-      {hasOverflow && (
-        <OnlineUserOverflow goToUser={goToUser} users={usersInOverflow} />
-      )}
-    </div>
-  )
-}
-
-const OnlineUserWidget = ({
-  user,
-  goToUser,
-  id,
-}: {
-  user: OnlineUser
-  goToUser: (user: OnlineUser) => void
-  id: string
-}) => {
-  const onClick = useCallback(() => {
-    goToUser(user)
-  }, [goToUser, user])
-  return (
-    <OLTooltip
-      id={id}
-      description={user.name}
-      overlayProps={{
-        placement: 'bottom',
-        trigger: ['hover', 'focus'],
-        delay: 0,
-      }}
-    >
-      <button className="online-users-row-button" onClick={onClick}>
-        <OnlineUserCircle user={user} />
-      </button>
-    </OLTooltip>
-  )
-}
-
-const OnlineUserCircle = ({ user }: { user: OnlineUser }) => {
-  const backgroundColor = getBackgroundColorForUserId(user.user_id)
-  const luminance = hslStringToLuminance(backgroundColor)
-  const [character] = [...user.name]
-  return (
-    <span
-      className={classNames('online-user-circle', {
-        'online-user-circle-light-font': luminance < 0.5,
-        'online-user-circle-dark-font': luminance >= 0.5,
-      })}
-      style={{ backgroundColor }}
-    >
-      {character}
-    </span>
-  )
-}
-
-const OnlineUserOverflow = ({
-  goToUser,
-  users,
-}: {
-  goToUser: (user: OnlineUser) => void
-  users: OnlineUser[]
-}) => {
-  const { t } = useTranslation()
-  return (
-    <Dropdown align="end">
-      <DropdownToggle className="online-users-row-button online-user-overflow-toggle">
-        <OLTooltip
-          id="connected-users"
-          description={t('n_more_collaborators', { count: users.length })}
-          overlayProps={{ placement: 'bottom' }}
-        >
-          <span className="online-user-circle">+{users.length}</span>
-        </OLTooltip>
-      </DropdownToggle>
-      <DropdownMenu className="online-user-overflow-dropdown">
-        <DropdownHeader aria-hidden="true">
-          {t('connected_users')}
-        </DropdownHeader>
-        {users.map((user, index) => (
-          <li role="none" key={`${user.user_id}_${index}`}>
-            <DropdownItem
-              as="button"
-              tabIndex={-1}
-              onClick={() => goToUser(user)}
-            >
-              <OnlineUserCircle user={user} /> {user.name}
-            </DropdownItem>
-          </li>
-        ))}
-      </DropdownMenu>
-    </Dropdown>
-  )
-}

+ 1 - 1
services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx

@@ -5,7 +5,7 @@ export const MenuBar: FC<
   React.PropsWithChildren<HTMLProps<HTMLDivElement> & { id: string }>
 > = ({ children, id, ...props }) => {
   return (
-    <div {...props}>
+    <div {...props} role="menubar">
       <NestableDropdownContextProvider id={id}>
         {children}
       </NestableDropdownContextProvider>

+ 0 - 42
services/web/frontend/stories/editor-navigation-toolbar.stories.jsx

@@ -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 <ToolbarHeader {...args} />
-}
-UpToThreeConnectedUsers.args = {
-  onlineUsers: ['a', 'c', 'd'].map(c => ({
-    user_id: c,
-    name: `${c}_user name`,
-  })),
-}
-
-export const ManyConnectedUsers = args => {
-  return <ToolbarHeader {...args} />
-}
-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],
-}

+ 0 - 35
services/web/frontend/stories/editor/layout-dropdown-button.stories.tsx

@@ -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<typeof LayoutDropdownButtonUi>
-) => (
-  <div className="toolbar toolbar-header justify-content-end m-4">
-    <div className="toolbar-right">
-      <LayoutDropdownButtonUi {...props} />
-    </div>
-  </div>
-)
-
-const meta: Meta<typeof LayoutDropdownButtonUi> = {
-  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

+ 3 - 21
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 (
     <div
@@ -48,24 +47,7 @@ export const OnlineUsersRedesign = ({ users }: { users: number }) => {
   )
 }
 
-export const OnlineUsersOld = ({ users }: { users: number }) => {
-  const generatedUsers = Array.from({ length: users }, generateUser)
-  return (
-    <div
-      style={{
-        backgroundColor: 'var(--online-users-border-color)',
-        padding: '20px',
-      }}
-    >
-      <OnlineUsersWidgetOld
-        onlineUsers={generatedUsers}
-        goToUser={(async () => {}) as any}
-      />
-    </div>
-  )
-}
-
-const meta: Meta<typeof OnlineUsersRedesign> = {
+const meta: Meta<typeof OnlineUsers> = {
   title: 'Editor / Online Users Widget',
   args: {
     users: 6,

+ 0 - 32
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;

+ 2 - 98
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);

+ 1 - 7
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 more</0> 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</0>.",
+  "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",

+ 0 - 26
services/web/test/frontend/features/editor-navigation-toolbar/components/chat-toggle-button.test.tsx

@@ -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('<ChatToggleButton />', function () {
-  const defaultProps = {
-    chatIsOpen: false,
-    unreadMessageCount: 0,
-    onClick: () => {},
-  }
-
-  it('displays the number of unread messages', function () {
-    const props = {
-      ...defaultProps,
-      unreadMessageCount: 113,
-    }
-    render(<ChatToggleButton {...props} />)
-    screen.getByText('113')
-  })
-
-  it("doesn't display the unread messages badge when the number of unread messages is zero", function () {
-    render(<ChatToggleButton {...defaultProps} />)
-    expect(screen.queryByText('0')).to.not.exist
-  })
-})

+ 0 - 237
services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.tsx

@@ -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('<LayoutDropdownButton />', function () {
-  let openStub: sinon.SinonStub
-  let sendMBSpy: sinon.SinonSpy
-
-  const defaultLayout: Partial<LayoutContextOwnStates> = {
-    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(<LayoutDropdownButton />, {
-      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(<LayoutDropdownButton />, {
-      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(<LayoutDropdownButton />, {
-      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(<LayoutDropdownButton />, {
-        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(<LayoutDropdownButton />, {
-        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)',
-      })
-    })
-  })
-})

+ 67 - 69
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('<OnlineUsersWidget />', 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(<OnlineUsersWidget {...defaultProps} />)
-      screen.getByText('t')
       screen.getByText('a')
+      screen.getByText('b')
     })
 
     it('displays user name in a tooltip', async function () {
       render(<OnlineUsersWidget {...defaultProps} />)
-      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(<OnlineUsersWidget {...props} />)
+      render(<OnlineUsersWidget {...defaultProps} />)
 
-      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(<OnlineUsersWidget {...props} />)
-      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 user names on hover', function () {
+    it('displays a maximum of 4 user initials and an overflow icon', function () {
       render(<OnlineUsersWidget {...props} />)
+      screen.getByText('a')
+      screen.getByText('b')
+      screen.getByText('c')
+      screen.getByText('d')
+      screen.getByText('+3')
+    })
 
-      const toggleButton = screen.getByRole('button')
-      fireEvent.click(toggleButton)
+    it('displays the remaining users in a dropdown when the overflow icon is clicked', async function () {
+      render(<OnlineUsersWidget {...props} />)
+      const overflowButton = screen.getByText('+3')
+      fireEvent.click(overflowButton)
 
-      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(<OnlineUsersWidget {...testProps} />)
 
-      const toggleButton = screen.getByRole('button')
-      fireEvent.click(toggleButton)
+      render(<OnlineUsersWidget {...props} />)
+      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',
       })
     })
   })

+ 0 - 78
services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.tsx

@@ -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('<ProjectNameEditableLabel />', function () {
-  const defaultProps = { projectName: 'test-project', onChange: () => {} }
-
-  it('displays the project name', function () {
-    render(<ProjectNameEditableLabel {...defaultProps} />)
-    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(<ProjectNameEditableLabel {...editableProps} />)
-      fireEvent.click(screen.getByRole('button'))
-      screen.getByRole('textbox')
-    })
-
-    it('displays an editable input when the project name is double clicked', function () {
-      render(<ProjectNameEditableLabel {...editableProps} />)
-      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(<ProjectNameEditableLabel {...props} />)
-
-      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(<ProjectNameEditableLabel {...props} />)
-
-      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(<ProjectNameEditableLabel {...nonEditableProps} />)
-      expect(screen.queryByRole('button')).to.not.exist
-    })
-
-    it('does not display an editable input when the project name is double clicked', function () {
-      render(<ProjectNameEditableLabel {...nonEditableProps} />)
-      fireEvent.doubleClick(screen.getByText('test-project'))
-      expect(screen.queryByRole('textbox')).to.not.exist
-    })
-  })
-})

+ 0 - 121
services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.tsx

@@ -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('<ToolbarHeader />', 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(<ToolbarHeader {...defaultProps} />)
-      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(<ToolbarHeader {...props} />)
-      screen.getByRole('link', { name: 'variation' })
-    })
-  })
-
-  describe('track changes toggle button', function () {
-    it('is displayed by default', function () {
-      renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
-      screen.getByText('Review')
-    })
-
-    it('is not displayed when "trackChangesVisible" prop is set to false', function () {
-      const props = {
-        ...defaultProps,
-        trackChangesVisible: false,
-      }
-      renderWithEditorContext(<ToolbarHeader {...props} />)
-      expect(screen.queryByText('Review')).to.not.exist
-    })
-  })
-
-  describe('History toggle button', function () {
-    it('is displayed by default', function () {
-      renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
-      screen.getByText('History')
-    })
-
-    it('is not displayed when "isRestrictedTokenMember" prop is set to true', function () {
-      const props = {
-        ...defaultProps,
-        isRestrictedTokenMember: true,
-      }
-      renderWithEditorContext(<ToolbarHeader {...props} />)
-      expect(screen.queryByText('History')).to.not.exist
-    })
-  })
-
-  describe('Chat toggle button', function () {
-    it('is displayed by default', function () {
-      renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
-      screen.getByText('Chat')
-    })
-
-    it('is not displayed when "chatVisible" prop is set to false', function () {
-      const props = {
-        ...defaultProps,
-        chatVisible: false,
-      }
-      renderWithEditorContext(<ToolbarHeader {...props} />)
-      expect(screen.queryByText('Chat')).to.not.exist
-    })
-  })
-
-  describe('Publish button', function () {
-    it('is displayed by default', function () {
-      renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
-      screen.getByText('Submit')
-    })
-
-    it('is not displayed for users with no publish permissions', function () {
-      const props = {
-        ...defaultProps,
-        hasPublishPermissions: false,
-      }
-      renderWithEditorContext(<ToolbarHeader {...props} />)
-      expect(screen.queryByText('Submit')).to.not.exist
-    })
-  })
-})

BIN
services/web/test/frontend/features/ide-react/unit/cobranding-logo.png


+ 248 - 0
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('<Toolbar />', 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(
+        <EditorProviders
+          providers={{ EditorProvider: makeEditorProvider({ cobranding }) }}
+        >
+          <Toolbar />
+        </EditorProviders>
+      )
+    })
+
+    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(
+        <EditorProviders
+          providers={{
+            EditorProvider: makeEditorProvider({ cobranding: undefined }),
+          }}
+        >
+          <Toolbar />
+        </EditorProviders>
+      )
+    })
+
+    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(
+      <EditorProviders projectName="My title">
+        <Toolbar />
+      </EditorProviders>
+    )
+    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(
+        <EditorProviders
+          projectName="My title"
+          providers={{
+            EditorProvider: makeEditorProvider({
+              renameProject: cy.stub().as('rename-project'),
+            }),
+          }}
+        >
+          <Toolbar />
+        </EditorProviders>
+      )
+    })
+
+    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(
+        <EditorProviders
+          projectName="My title"
+          providers={{
+            EditorProvider: makeEditorProvider({
+              isRestrictedTokenMember: false,
+            }),
+          }}
+        >
+          <Toolbar />
+        </EditorProviders>
+      )
+      cy.findByRole('button', { name: 'History' }).should('be.visible').click()
+    })
+
+    it('Should not show history button to restricted token members', function () {
+      cy.mount(
+        <EditorProviders
+          projectName="My title"
+          providers={{
+            EditorProvider: makeEditorProvider({
+              isRestrictedTokenMember: true,
+            }),
+          }}
+        >
+          <Toolbar />
+        </EditorProviders>
+      )
+      cy.findByRole('button', { name: 'History' }).should('not.exist')
+    })
+  })
+
+  it('should show share button', function () {
+    cy.mount(
+      <EditorProviders projectName="My title">
+        <Toolbar />
+      </EditorProviders>
+    )
+    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(
+      <EditorProviders projectName="My title">
+        <Toolbar />
+      </EditorProviders>
+    )
+    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(
+        <EditorProviders permissionsLevel="readOnly">
+          <Toolbar />
+        </EditorProviders>
+      )
+      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(
+        <EditorProviders>
+          <Toolbar />
+        </EditorProviders>
+      )
+    })
+
+    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
+  })
+})

+ 15 - 2
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<PropsWithChildren> = ({ 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 (