first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import OLButton from '@/features/ui/components/ol/ol-button'
function BackToEditorButton({ onClick }: { onClick: () => void }) {
const { t } = useTranslation()
return (
<OLButton
variant="secondary"
size="sm"
onClick={onClick}
className="back-to-editor-btn"
>
<MaterialIcon type="arrow_back" className="toolbar-btn-secondary-icon" />
<span className="toolbar-label">{t('back_to_editor')}</span>
</OLButton>
)
}
export default BackToEditorButton

View File

@@ -0,0 +1,35 @@
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import OLTooltip from '@/features/ui/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

View File

@@ -0,0 +1,35 @@
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import OLBadge from '@/features/ui/components/ol/ol-badge'
function ChatToggleButton({ chatIsOpen, unreadMessageCount, onClick }) {
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>
)
}
ChatToggleButton.propTypes = {
chatIsOpen: PropTypes.bool,
unreadMessageCount: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
}
export default ChatToggleButton

View File

@@ -0,0 +1,30 @@
import PropTypes from 'prop-types'
function CobrandingLogo({
brandVariationHomeUrl,
brandVariationName,
logoImgUrl,
}) {
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>
)
}
CobrandingLogo.propTypes = {
brandVariationHomeUrl: PropTypes.string.isRequired,
brandVariationName: PropTypes.string.isRequired,
logoImgUrl: PropTypes.string.isRequired,
}
export default CobrandingLogo

View File

@@ -0,0 +1,133 @@
import React, { useState, useCallback } from 'react'
import ToolbarHeader from './toolbar-header'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useChatContext } from '../../chat/context/chat-context'
import { useLayoutContext } from '../../../shared/context/layout-context'
import { useProjectContext } from '../../../shared/context/project-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { Doc } from '../../../../../types/doc'
function isOpentoString(open: boolean) {
return open ? 'open' : 'close'
}
const EditorNavigationToolbarRoot = React.memo(
function EditorNavigationToolbarRoot({
onlineUsersArray,
openDoc,
openShareProjectModal,
}: {
onlineUsersArray: any[]
openDoc: (doc: Doc, { gotoLine }: { gotoLine: number }) => void
openShareProjectModal: () => void
}) {
const {
name: projectName,
features: { trackChangesVisible },
} = useProjectContext()
const {
cobranding,
isRestrictedTokenMember,
renameProject,
permissionsLevel,
} = useEditorContext()
const {
chatIsOpen,
setChatIsOpen,
reviewPanelOpen,
setReviewPanelOpen,
view,
setView,
setLeftMenuShown,
} = useLayoutContext()
const { markMessagesAsRead, unreadMessageCount } = useChatContext()
const toggleChatOpen = useCallback(() => {
if (!chatIsOpen) {
markMessagesAsRead()
}
eventTracking.sendMB('navigation-clicked-chat', {
action: isOpentoString(!chatIsOpen),
})
setChatIsOpen(!chatIsOpen)
}, [chatIsOpen, setChatIsOpen, markMessagesAsRead])
const toggleReviewPanelOpen = useCallback(
event => {
event.preventDefault()
eventTracking.sendMB('navigation-clicked-review', {
action: isOpentoString(!reviewPanelOpen),
})
setReviewPanelOpen(value => !value)
},
[reviewPanelOpen, setReviewPanelOpen]
)
const [shouldReopenChat, setShouldReopenChat] = useState(chatIsOpen)
const toggleHistoryOpen = useCallback(() => {
const action = view === 'history' ? 'close' : 'open'
eventTracking.sendMB('navigation-clicked-history', { action })
if (chatIsOpen && action === 'open') {
setShouldReopenChat(true)
toggleChatOpen()
}
if (shouldReopenChat && action === 'close') {
setShouldReopenChat(false)
toggleChatOpen()
}
setView(view === 'history' ? 'editor' : 'history')
}, [view, chatIsOpen, shouldReopenChat, setView, toggleChatOpen])
const openShareModal = useCallback(() => {
eventTracking.sendMB('navigation-clicked-share')
openShareProjectModal()
}, [openShareProjectModal])
const onShowLeftMenuClick = useCallback(() => {
eventTracking.sendMB('navigation-clicked-menu')
setLeftMenuShown(value => !value)
}, [setLeftMenuShown])
const goToUser = useCallback(
user => {
if (user.doc && typeof user.row === 'number') {
openDoc(user.doc, { gotoLine: user.row + 1 })
}
},
[openDoc]
)
return (
<ToolbarHeader
// @ts-ignore: TODO(convert ToolbarHeader to TSX)
cobranding={cobranding}
onShowLeftMenuClick={onShowLeftMenuClick}
chatIsOpen={chatIsOpen}
unreadMessageCount={unreadMessageCount}
toggleChatOpen={toggleChatOpen}
reviewPanelOpen={reviewPanelOpen}
toggleReviewPanelOpen={toggleReviewPanelOpen}
historyIsOpen={view === 'history'}
toggleHistoryOpen={toggleHistoryOpen}
onlineUsers={onlineUsersArray}
goToUser={goToUser}
isRestrictedTokenMember={isRestrictedTokenMember}
hasPublishPermissions={
permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite'
}
chatVisible={!isRestrictedTokenMember}
projectName={projectName}
renameProject={renameProject}
hasRenamePermissions={permissionsLevel === 'owner'}
openShareModal={openShareModal}
trackChangesVisible={trackChangesVisible}
/>
)
}
)
export default EditorNavigationToolbarRoot

View File

@@ -0,0 +1,23 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
function HistoryToggleButton({ onClick }) {
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>
)
}
HistoryToggleButton.propTypes = {
onClick: PropTypes.func.isRequired,
}
export default HistoryToggleButton

View File

@@ -0,0 +1,285 @@
import { memo, useCallback, forwardRef } from 'react'
import { Spinner } from 'react-bootstrap-5'
import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
DropdownToggleCustom,
} from '@/features/ui/components/bootstrap-5/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 useEventListener from '../../../shared/hooks/use-event-listener'
import { DetachRole } from '@/shared/context/detach-context'
import MaterialIcon from '@/shared/components/material-icon'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
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 {
reattach,
detach,
detachIsLinked,
detachRole,
changeLayout,
view,
pdfLayout,
} = useLayoutContext()
const handleDetach = useCallback(() => {
detach()
eventTracking.sendMB('project-layout-detach')
}, [detach])
const handleReattach = useCallback(() => {
if (detachRole !== 'detacher') {
return
}
reattach()
eventTracking.sendMB('project-layout-reattach')
}, [detachRole, reattach])
// reattach when the PDF pane opens
useEventListener('ui:pdf-open', handleReattach)
const handleChangeLayout = useCallback(
(newLayout: IdeLayout, newView?: IdeView) => {
handleReattach()
changeLayout(newLayout, newView)
eventTracking.sendMB('project-layout-change', {
layout: newLayout,
view: newView,
})
},
[changeLayout, handleReattach]
)
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 (
<>
{processing && (
<div aria-live="assertive" className="sr-only">
{t('layout_processing')}
</div>
)}
<Dropdown className="toolbar-item layout-dropdown" align="end">
<DropdownToggle
id="layout-dropdown-btn"
className="btn-full-height"
as={LayoutDropdownToggleButton}
>
{processing ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : (
<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'
) : (
<span className="spinner-container">
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
<span className="visually-hidden">{t('loading')}</span>
</span>
)
) : null
}
leadingIcon="select_window"
>
{t('pdf_in_separate_tab')}
</EnhancedDropdownItem>
) : (
<BS5DetachDisabled />
)}
</DropdownMenu>
</Dropdown>
</>
)
}
export default memo(LayoutDropdownButton)

View File

@@ -0,0 +1,22 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
function MenuButton({ onClick }) {
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>
)
}
MenuButton.propTypes = {
onClick: PropTypes.func.isRequired,
}
export default MenuButton

View File

@@ -0,0 +1,133 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import {
Dropdown,
DropdownHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { getBackgroundColorForUserId } from '@/shared/utils/colors'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon from '@/shared/components/material-icon'
function OnlineUsersWidget({ onlineUsers, goToUser }) {
const { t } = useTranslation()
const shouldDisplayDropdown = onlineUsers.length >= 4
if (shouldDisplayDropdown) {
return (
<Dropdown id="online-users" className="online-users" align="end">
<DropdownToggle
as={DropDownToggleButton}
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'] }}
>
<span>
{/* OverlayTrigger won't fire unless UserIcon is wrapped in a span */}
<UserIcon user={user} onClick={goToUser} />
</span>
</OLTooltip>
))}
</div>
)
}
}
OnlineUsersWidget.propTypes = {
onlineUsers: PropTypes.arrayOf(
PropTypes.shape({
user_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})
).isRequired,
goToUser: PropTypes.func.isRequired,
}
function UserIcon({ user, showName, onClick }) {
const backgroundColor = getBackgroundColorForUserId(user.user_id)
function handleOnClick() {
onClick?.(user)
}
const [character] = [...user.name]
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<span onClick={handleOnClick}>
<span className="online-user" style={{ backgroundColor }}>
{character}
</span>
{showName && user.name}
</span>
)
}
UserIcon.propTypes = {
user: PropTypes.shape({
user_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}),
showName: PropTypes.bool,
onClick: PropTypes.func,
}
const DropDownToggleButton = React.forwardRef((props, ref) => {
const { t } = useTranslation()
return (
<OLTooltip
id="connected-users"
description={t('connected_users')}
overlayProps={{ placement: 'left' }}
>
<button
type="button"
className="online-user online-user-multi"
onClick={props.onClick} // required by Bootstrap Dropdown to trigger an opening
ref={ref}
>
<strong>{props.onlineUserCount}</strong>&nbsp;
<MaterialIcon type="groups" />
</button>
</OLTooltip>
)
})
DropDownToggleButton.displayName = 'DropDownToggleButton'
DropDownToggleButton.propTypes = {
onlineUserCount: PropTypes.number.isRequired,
onClick: PropTypes.func,
}
export default OnlineUsersWidget

View File

@@ -0,0 +1,99 @@
import { useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import OLTooltip from '@/features/ui/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

View File

@@ -0,0 +1,23 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
function ShareProjectButton({ onClick }) {
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>
)
}
ShareProjectButton.propTypes = {
onClick: PropTypes.func.isRequired,
}
export default ShareProjectButton

View File

@@ -0,0 +1,157 @@
import React from 'react'
import PropTypes from 'prop-types'
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 { canUseNewEditor } from '@/features/ide-redesign/utils/new-editor-utils'
import TryNewEditorButton from '../try-new-editor-button'
const [publishModalModules] = importOverleafModules('publishModal')
const PublishButton = publishModalModules?.import.default
const offlineModeToolbarButtons = importOverleafModules(
'offlineModeToolbarButtons'
)
// double opt-in
const enableROMirrorOnClient =
isSplitTestEnabled('ro-mirror-on-client') &&
new URLSearchParams(window.location.search).get('ro-mirror-on-client') ===
'enabled'
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,
}) {
const chatEnabled = getMeta('ol-chatEnabled')
const { t } = useTranslation()
const shouldDisplayPublishButton = hasPublishPermissions && PublishButton
return (
<header
className="toolbar toolbar-header"
role="navigation"
aria-label={t('project_layout_sharing_submission')}
>
<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>
</header>
)
})
ToolbarHeader.propTypes = {
onShowLeftMenuClick: PropTypes.func.isRequired,
cobranding: PropTypes.object,
chatIsOpen: PropTypes.bool,
toggleChatOpen: PropTypes.func.isRequired,
reviewPanelOpen: PropTypes.bool,
toggleReviewPanelOpen: PropTypes.func.isRequired,
historyIsOpen: PropTypes.bool,
toggleHistoryOpen: PropTypes.func.isRequired,
unreadMessageCount: PropTypes.number.isRequired,
onlineUsers: PropTypes.array.isRequired,
goToUser: PropTypes.func.isRequired,
isRestrictedTokenMember: PropTypes.bool,
hasPublishPermissions: PropTypes.bool,
chatVisible: PropTypes.bool,
projectName: PropTypes.string.isRequired,
renameProject: PropTypes.func.isRequired,
hasRenamePermissions: PropTypes.bool,
openShareModal: PropTypes.func.isRequired,
trackChangesVisible: PropTypes.bool,
}
export default ToolbarHeader

View File

@@ -0,0 +1,39 @@
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
function TrackChangesToggleButton({
trackChangesIsOpen,
disabled,
onMouseDown,
}) {
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>
)
}
TrackChangesToggleButton.propTypes = {
trackChangesIsOpen: PropTypes.bool,
disabled: PropTypes.bool,
onMouseDown: PropTypes.func.isRequired,
}
export default TrackChangesToggleButton

View File

@@ -0,0 +1,27 @@
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import OLButton from '@/features/ui/components/ol/ol-button'
function UpgradePrompt() {
const { t } = useTranslation()
function handleClick(e) {
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

View File

@@ -0,0 +1,28 @@
import { useCallback } from 'react'
import OLButton from '../ui/components/ol/ol-button'
import { useIdeRedesignSwitcherContext } from '../ide-react/context/ide-redesign-switcher-context'
import MaterialIcon from '@/shared/components/material-icon'
import { useTranslation } from 'react-i18next'
const TryNewEditorButton = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const onClick = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
return (
<div className="d-flex align-items-center">
<OLButton
className="toolbar-experiment-button"
onClick={onClick}
size="sm"
leadingIcon={<MaterialIcon type="experiment" unfilled />}
variant="info"
>
{t('try_the_new_editor')}
</OLButton>
</div>
)
}
export default TryNewEditorButton