mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01: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
-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
|
||||
Reference in New Issue
Block a user