mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-10 06:39:01 +02:00
Merge pull request #31397 from overleaf/mj-old-editor-toolbar-teardown
[web] Tear down old editor toolbar GitOrigin-RevId: 8ba74abcc56e7bd476a9d6cae72f38486168c2ed
This commit is contained in:
committed by
Copybot
parent
4f284e15d5
commit
cc1dedca7d
@@ -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": "",
|
||||
|
||||
-35
@@ -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
|
||||
-36
@@ -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
|
||||
-26
@@ -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
|
||||
-18
@@ -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
|
||||
-241
@@ -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)
|
||||
-17
@@ -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
|
||||
+122
-107
@@ -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
|
||||
|
||||
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}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
<span onClick={handleOnClick}>
|
||||
<span className="online-user" style={{ backgroundColor }}>
|
||||
{character}
|
||||
</span>
|
||||
{showName && user.name}
|
||||
<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 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>
|
||||
<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
|
||||
}
|
||||
|
||||
-99
@@ -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
|
||||
-18
@@ -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
|
||||
-162
@@ -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
|
||||
-36
@@ -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
|
||||
-27
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
-146
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
-26
@@ -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
|
||||
})
|
||||
})
|
||||
-237
@@ -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)',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+68
-70
@@ -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 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')
|
||||
})
|
||||
|
||||
it('displays user names on hover', function () {
|
||||
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)
|
||||
|
||||
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(<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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
-78
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
-121
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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
|
||||
})
|
||||
})
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user