mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
Editor Redesign Cleanup: main layout, toolbar, and rail (#31031)
* feat: integrate main layout, toolbar, and rail from redesign into main ide-react folder * feat: remove additional files no longer used after ide redesign GitOrigin-RevId: 8fd77f63cb9c67be91995a9dde13b0fe2376d80f
This commit is contained in:
committed by
Copybot
parent
c790449196
commit
fc8d564320
+44
@@ -0,0 +1,44 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import ChangeLayoutOptions from './change-layout-options'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
|
||||
export default function ChangeLayoutButton() {
|
||||
const { t } = useTranslation()
|
||||
const toggleButtonClassName = classNames(
|
||||
'ide-redesign-toolbar-button-subdued',
|
||||
'ide-redesign-toolbar-dropdown-toggle-subdued',
|
||||
'ide-redesign-toolbar-button-icon'
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="ide-redesign-toolbar-button-container">
|
||||
<Dropdown className="toolbar-item layout-dropdown" align="end">
|
||||
<OLTooltip
|
||||
id="tooltip-open-layout-options"
|
||||
description={t('layout_options')}
|
||||
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||
>
|
||||
<span>
|
||||
<DropdownToggle
|
||||
id="layout-dropdown-btn"
|
||||
className={toggleButtonClassName}
|
||||
aria-label={t('layout_options')}
|
||||
>
|
||||
<MaterialIcon type="space_dashboard" unfilled />
|
||||
</DropdownToggle>
|
||||
</span>
|
||||
</OLTooltip>
|
||||
<DropdownMenu>
|
||||
<ChangeLayoutOptions />
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
DropdownItem,
|
||||
DropdownHeader,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import {
|
||||
IdeLayout,
|
||||
IdeView,
|
||||
useLayoutContext,
|
||||
} from '@/shared/context/layout-context'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DetachRole } from '@/shared/context/detach-context'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
import { Shortcut } from '@/shared/components/shortcut'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
|
||||
|
||||
const getActiveLayoutOption = ({
|
||||
pdfLayout,
|
||||
view,
|
||||
detachRole,
|
||||
}: {
|
||||
pdfLayout: IdeLayout
|
||||
view: IdeView | null
|
||||
detachRole?: DetachRole
|
||||
}): LayoutOption | null => {
|
||||
if (view === 'history') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (detachRole === 'detacher') {
|
||||
return 'detachedPdf'
|
||||
}
|
||||
|
||||
if (pdfLayout === 'flat' && (view === 'editor' || view === 'file')) {
|
||||
return 'editorOnly'
|
||||
}
|
||||
|
||||
if (pdfLayout === 'flat' && view === 'pdf') {
|
||||
return 'pdfOnly'
|
||||
}
|
||||
|
||||
if (pdfLayout === 'sideBySide') {
|
||||
return 'sideBySide'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const LayoutDropdownItem = ({
|
||||
active,
|
||||
disabled = false,
|
||||
processing = false,
|
||||
leadingIcon,
|
||||
trailingIcon,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean
|
||||
leadingIcon: React.ReactNode
|
||||
trailingIcon?: React.ReactNode
|
||||
onClick: () => void
|
||||
children: React.ReactNode
|
||||
processing?: boolean
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
if (processing) {
|
||||
leadingIcon = <OLSpinner size="sm" />
|
||||
} else if (active) {
|
||||
leadingIcon = 'check'
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
active={active}
|
||||
aria-current={active}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
leadingIcon={leadingIcon}
|
||||
trailingIcon={trailingIcon}
|
||||
className={classNames({ 'dropdown-item-wide': isMac })}
|
||||
>
|
||||
{children}
|
||||
</DropdownItem>
|
||||
)
|
||||
}
|
||||
|
||||
const shortcuts: Record<LayoutOption, string[] | null> = isMac
|
||||
? {
|
||||
editorOnly: ['⌃', '⌘', '←'],
|
||||
pdfOnly: ['⌃', '⌘', '→'],
|
||||
sideBySide: ['⌃', '⌘', '↓'],
|
||||
detachedPdf: ['⌃', '⌘', '↑'],
|
||||
}
|
||||
: {
|
||||
editorOnly: null,
|
||||
pdfOnly: null,
|
||||
sideBySide: null,
|
||||
detachedPdf: null,
|
||||
}
|
||||
|
||||
export default function ChangeLayoutOptions() {
|
||||
const {
|
||||
detachIsLinked,
|
||||
detachRole,
|
||||
view,
|
||||
pdfLayout,
|
||||
handleChangeLayout,
|
||||
handleDetach,
|
||||
} = useLayoutContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const detachable = 'BroadcastChannel' in window
|
||||
|
||||
const activeLayoutOption = getActiveLayoutOption({
|
||||
pdfLayout,
|
||||
view,
|
||||
detachRole,
|
||||
})
|
||||
|
||||
const waitingForDetachedLink = !detachIsLinked && detachRole === 'detacher'
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownHeader>{t('layout_options')}</DropdownHeader>
|
||||
<LayoutDropdownItem
|
||||
onClick={() => handleChangeLayout('sideBySide')}
|
||||
active={activeLayoutOption === 'sideBySide'}
|
||||
leadingIcon="splitscreen_right"
|
||||
trailingIcon={
|
||||
shortcuts.sideBySide && <Shortcut keys={shortcuts.sideBySide} />
|
||||
}
|
||||
>
|
||||
{t('split_view')}
|
||||
</LayoutDropdownItem>
|
||||
<LayoutDropdownItem
|
||||
onClick={() => handleChangeLayout('flat', 'editor')}
|
||||
active={activeLayoutOption === 'editorOnly'}
|
||||
leadingIcon="edit"
|
||||
trailingIcon={
|
||||
shortcuts.editorOnly && <Shortcut keys={shortcuts.editorOnly} />
|
||||
}
|
||||
>
|
||||
{t('editor_only')}
|
||||
</LayoutDropdownItem>
|
||||
<LayoutDropdownItem
|
||||
onClick={() => handleChangeLayout('flat', 'pdf')}
|
||||
active={activeLayoutOption === 'pdfOnly'}
|
||||
leadingIcon="picture_as_pdf"
|
||||
trailingIcon={
|
||||
shortcuts.pdfOnly && <Shortcut keys={shortcuts.pdfOnly} />
|
||||
}
|
||||
>
|
||||
{t('pdf_only')}
|
||||
</LayoutDropdownItem>
|
||||
<LayoutDropdownItem
|
||||
onClick={() => handleDetach()}
|
||||
active={activeLayoutOption === 'detachedPdf' && detachIsLinked}
|
||||
disabled={!detachable}
|
||||
leadingIcon="open_in_new"
|
||||
trailingIcon={
|
||||
shortcuts.detachedPdf && <Shortcut keys={shortcuts.detachedPdf} />
|
||||
}
|
||||
processing={waitingForDetachedLink}
|
||||
>
|
||||
{t('open_pdf_in_separate_tab')}
|
||||
</LayoutDropdownItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
Command,
|
||||
formatShortcut,
|
||||
Shortcuts,
|
||||
useCommandRegistry,
|
||||
} from '@/features/ide-react/context/command-registry-context'
|
||||
import {
|
||||
DropdownDivider,
|
||||
DropdownHeader,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import {
|
||||
MenuBarDropdown,
|
||||
NestedMenuBarDropdown,
|
||||
} from '@/shared/components/menu-bar/menu-bar-dropdown'
|
||||
import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
|
||||
import { Fragment, useCallback, useMemo } from 'react'
|
||||
|
||||
type CommandId = string
|
||||
type TaggedCommand = Command & {
|
||||
type: 'command'
|
||||
shortcuts?: Shortcuts[CommandId]
|
||||
}
|
||||
type Entry<T> = T | GroupStructure<T>
|
||||
type GroupStructure<T> = {
|
||||
id: string
|
||||
title: string
|
||||
children: Array<Entry<T>>
|
||||
}
|
||||
export type MenuSectionStructure<T = CommandId> = {
|
||||
title?: string
|
||||
id: string
|
||||
children: Array<Entry<T>>
|
||||
}
|
||||
export type MenuStructure<T = CommandId> = Array<MenuSectionStructure<T>>
|
||||
|
||||
const CommandDropdown = ({
|
||||
menu,
|
||||
title,
|
||||
id,
|
||||
}: {
|
||||
menu: MenuStructure<CommandId>
|
||||
title: string
|
||||
id: string
|
||||
}) => {
|
||||
const { registry, shortcuts } = useCommandRegistry()
|
||||
const populatedSections = useMemo(
|
||||
() =>
|
||||
menu
|
||||
.map(section => populateSectionOrGroup(section, registry, shortcuts))
|
||||
.filter(x => x.children.length > 0),
|
||||
[menu, registry, shortcuts]
|
||||
)
|
||||
|
||||
if (populatedSections.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuBarDropdown
|
||||
title={title}
|
||||
id={id}
|
||||
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
|
||||
>
|
||||
{populatedSections.map((section, index) => {
|
||||
return (
|
||||
<Fragment key={section.id}>
|
||||
<CommandSectionContent
|
||||
section={section}
|
||||
includeDivider={index > 0}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</MenuBarDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const CommandSection = ({
|
||||
section: sectionStructure,
|
||||
includeDivider = true,
|
||||
}: {
|
||||
section: MenuSectionStructure<CommandId>
|
||||
includeDivider?: boolean
|
||||
}) => {
|
||||
const { registry, shortcuts } = useCommandRegistry()
|
||||
const section = populateSectionOrGroup(sectionStructure, registry, shortcuts)
|
||||
return (
|
||||
<CommandSectionContent section={section} includeDivider={includeDivider} />
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSectionContent({
|
||||
section,
|
||||
includeDivider = true,
|
||||
}: {
|
||||
section: MenuSectionStructure<TaggedCommand>
|
||||
includeDivider?: boolean
|
||||
}) {
|
||||
if (section.children.length === 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{includeDivider && <DropdownDivider />}
|
||||
{section.title && <DropdownHeader>{section.title}</DropdownHeader>}
|
||||
{section.children.map(child => (
|
||||
<CommandDropdownChild item={child} key={child.id} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandDropdownChild = ({ item }: { item: Entry<TaggedCommand> }) => {
|
||||
const onClickHandler = useCallback(() => {
|
||||
if (isTaggedCommand(item)) {
|
||||
item.handler?.({ location: 'menu-bar' })
|
||||
}
|
||||
}, [item])
|
||||
|
||||
if (isTaggedCommand(item)) {
|
||||
return (
|
||||
<MenuBarOption
|
||||
eventKey={item.id}
|
||||
key={item.id}
|
||||
title={item.label}
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick={onClickHandler}
|
||||
href={item.href}
|
||||
disabled={item.disabled}
|
||||
trailingIcon={
|
||||
item.shortcuts && <span>{formatShortcut(item.shortcuts[0])}</span>
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<NestedMenuBarDropdown title={item.title} id={item.id} key={item.id}>
|
||||
{item.children.map(subChild => {
|
||||
return <CommandDropdownChild item={subChild} key={subChild.id} />
|
||||
})}
|
||||
</NestedMenuBarDropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CommandDropdown
|
||||
|
||||
function populateSectionOrGroup<
|
||||
T extends { children: Array<Entry<CommandId>> },
|
||||
>(
|
||||
section: T,
|
||||
registry: Map<string, Command>,
|
||||
shortcuts: Shortcuts
|
||||
): Omit<T, 'children'> & {
|
||||
children: Array<Entry<TaggedCommand>>
|
||||
} {
|
||||
const { children, ...rest } = section
|
||||
return {
|
||||
...rest,
|
||||
children: children
|
||||
.map(child => {
|
||||
if (typeof child !== 'string') {
|
||||
const populatedChild = populateSectionOrGroup(
|
||||
child,
|
||||
registry,
|
||||
shortcuts
|
||||
)
|
||||
if (populatedChild.children.length === 0) {
|
||||
// Skip empty groups
|
||||
return undefined
|
||||
}
|
||||
return populatedChild
|
||||
}
|
||||
const command = registry.get(child)
|
||||
if (command) {
|
||||
return {
|
||||
...command,
|
||||
shortcuts: shortcuts[command.id],
|
||||
type: 'command' as const,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
.filter(x => x !== undefined),
|
||||
}
|
||||
}
|
||||
|
||||
function isTaggedCommand(item: Entry<TaggedCommand>): item is TaggedCommand {
|
||||
return 'type' in item && item.type === 'command'
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
|
||||
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import { isSmallDevice, sendMB } from '@/infrastructure/event-tracking'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
|
||||
export const DownloadProjectZip = () => {
|
||||
const { t } = useTranslation()
|
||||
const { projectId } = useProjectContext()
|
||||
const sendDownloadEvent = useCallback(() => {
|
||||
sendMB('download-zip-button-click', {
|
||||
projectId,
|
||||
location: 'project-name-dropdown',
|
||||
isSmallDevice,
|
||||
})
|
||||
}, [projectId])
|
||||
|
||||
useCommandProvider(
|
||||
() => [
|
||||
{
|
||||
id: 'download-as-source-zip',
|
||||
href: `/project/${projectId}/download/zip`,
|
||||
label: t('download_as_source_zip'),
|
||||
},
|
||||
],
|
||||
[t, projectId]
|
||||
)
|
||||
|
||||
return (
|
||||
<OLDropdownMenuItem
|
||||
href={`/project/${projectId}/download/zip`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={sendDownloadEvent}
|
||||
>
|
||||
{t('download_as_source_zip')}
|
||||
</OLDropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export const DownloadProjectPDF = () => {
|
||||
const { t } = useTranslation()
|
||||
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
const sendDownloadEvent = useCallback(() => {
|
||||
sendEvent('download-pdf-button-click', {
|
||||
projectId,
|
||||
location: 'project-name-dropdown',
|
||||
isSmallDevice,
|
||||
})
|
||||
}, [projectId, sendEvent])
|
||||
|
||||
useCommandProvider(
|
||||
() => [
|
||||
{
|
||||
id: 'download-pdf',
|
||||
disabled: !pdfUrl,
|
||||
href: pdfDownloadUrl || pdfUrl,
|
||||
handler: ({ location }) => {
|
||||
sendEvent('download-pdf-button-click', {
|
||||
projectId,
|
||||
location,
|
||||
isSmallDevice,
|
||||
})
|
||||
},
|
||||
label: t('download_as_pdf'),
|
||||
},
|
||||
],
|
||||
[t, pdfUrl, projectId, pdfDownloadUrl, sendEvent]
|
||||
)
|
||||
|
||||
const button = (
|
||||
<OLDropdownMenuItem
|
||||
href={pdfDownloadUrl || pdfUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={sendDownloadEvent}
|
||||
disabled={!pdfUrl}
|
||||
>
|
||||
{t('download_as_pdf')}
|
||||
</OLDropdownMenuItem>
|
||||
)
|
||||
|
||||
if (!pdfUrl) {
|
||||
return (
|
||||
<OLTooltip
|
||||
id="tooltip-download-pdf-unavailable"
|
||||
description={t('please_compile_pdf_before_download')}
|
||||
overlayProps={{ placement: 'right', delay: 0 }}
|
||||
>
|
||||
<span>{button}</span>
|
||||
</OLTooltip>
|
||||
)
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import EditorCloneProjectModalWrapper from '@/features/clone-project-modal/components/editor-clone-project-modal-wrapper'
|
||||
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
import useOpenProject from '@/shared/hooks/use-open-project'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const DuplicateProject = () => {
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
const { t } = useTranslation()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const anonymous = getMeta('ol-anonymous')
|
||||
const openProject = useOpenProject()
|
||||
|
||||
const handleShowModal = useCallback(() => {
|
||||
sendEvent('copy-project', { location: 'project-title-dropdown' })
|
||||
setShowModal(true)
|
||||
}, [sendEvent])
|
||||
|
||||
if (anonymous) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLDropdownMenuItem onClick={handleShowModal}>
|
||||
{t('make_a_copy')}
|
||||
</OLDropdownMenuItem>
|
||||
<EditorCloneProjectModalWrapper
|
||||
show={showModal}
|
||||
handleHide={() => setShowModal(false)}
|
||||
openProject={openProject}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import OLFormControl from '@/shared/components/ol/ol-form-control'
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
type EditableLabelProps = {
|
||||
initialText: string
|
||||
className?: string
|
||||
onChange: (name: string) => void
|
||||
onCancel: () => void
|
||||
maxLength?: number
|
||||
}
|
||||
|
||||
const EditableLabel = ({
|
||||
initialText,
|
||||
className,
|
||||
onChange,
|
||||
onCancel,
|
||||
maxLength,
|
||||
}: EditableLabelProps) => {
|
||||
const [name, setName] = useState(initialText)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
useEffect(() => {
|
||||
inputRef.current?.select()
|
||||
}, [])
|
||||
|
||||
const onInputChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
event => {
|
||||
setName(event.target.value)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const finishRenaming = useCallback(() => {
|
||||
onChange(name)
|
||||
}, [onChange, name])
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
finishRenaming()
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
onCancel()
|
||||
}
|
||||
},
|
||||
[finishRenaming, onCancel]
|
||||
)
|
||||
|
||||
return (
|
||||
<OLFormControl
|
||||
className={className}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={onInputChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={finishRenaming}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default EditableLabel
|
||||
@@ -0,0 +1,46 @@
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Cobranding } from '../../../../../../types/cobranding'
|
||||
|
||||
type ToolbarLogosProps = {
|
||||
cobranding?: Cobranding
|
||||
}
|
||||
|
||||
export const ToolbarLogos = ({ cobranding }: ToolbarLogosProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="ide-redesign-toolbar-logos">
|
||||
<OLTooltip
|
||||
id="tooltip-home-button"
|
||||
description={t('back_to_your_projects')}
|
||||
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||
>
|
||||
<div className="ide-redesign-toolbar-home-button">
|
||||
<a href="/project" className="ide-redesign-toolbar-home-link">
|
||||
<span className="toolbar-ol-logo" aria-label={t('overleaf_logo')} />
|
||||
<MaterialIcon type="home" className="toolbar-ol-home-button" />
|
||||
</a>
|
||||
</div>
|
||||
</OLTooltip>
|
||||
{cobranding && cobranding.logoImgUrl && (
|
||||
<>
|
||||
<span className="ide-redesign-toolbar-cobranding-separator" />
|
||||
<a
|
||||
className="ide-redesign-toolbar-cobranding-link"
|
||||
href={cobranding.brandVariationHomeUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<img
|
||||
src={cobranding.logoImgUrl}
|
||||
className="ide-redesign-toolbar-cobranding-logo"
|
||||
alt={cobranding.brandVariationName}
|
||||
/>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import {
|
||||
DropdownDivider,
|
||||
DropdownHeader,
|
||||
DropdownItem,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import { MenuBar } from '@/shared/components/menu-bar/menu-bar'
|
||||
import { MenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown'
|
||||
import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ChangeLayoutOptions from './change-layout-options'
|
||||
import { ElementType, useCallback, useMemo, useState } from 'react'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
|
||||
import CommandDropdown, {
|
||||
CommandSection,
|
||||
MenuSectionStructure,
|
||||
MenuStructure,
|
||||
} from './command-dropdown'
|
||||
import { useRailContext } from '../../context/rail-context'
|
||||
import WordCountModal from '@/features/word-count-modal/components/word-count-modal'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
import EditorCloneProjectModalWrapper from '@/features/clone-project-modal/components/editor-clone-project-modal-wrapper'
|
||||
import useOpenProject from '@/shared/hooks/use-open-project'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
|
||||
const menubarExtraComponents = importOverleafModules(
|
||||
'menubarExtraComponents'
|
||||
) as {
|
||||
import: { default: ElementType }
|
||||
}[]
|
||||
|
||||
export const ToolbarMenuBar = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { setView, view } = useLayoutContext()
|
||||
const { pdfUrl } = useCompileContext()
|
||||
const wordCountEnabled = pdfUrl || isSplitTestEnabled('word-count-client')
|
||||
const [showWordCountModal, setShowWordCountModal] = useState(false)
|
||||
const [showCloneProjectModal, setShowCloneProjectModal] = useState(false)
|
||||
const openProject = useOpenProject()
|
||||
|
||||
const anonymous = getMeta('ol-anonymous')
|
||||
|
||||
useCommandProvider(
|
||||
() => [
|
||||
{
|
||||
type: 'command',
|
||||
label: t('show_version_history'),
|
||||
handler: () => {
|
||||
setView(view === 'history' ? 'editor' : 'history')
|
||||
},
|
||||
id: 'show_version_history',
|
||||
},
|
||||
{
|
||||
type: 'command',
|
||||
label: t('word_count_lower'),
|
||||
disabled: !wordCountEnabled,
|
||||
handler: () => {
|
||||
setShowWordCountModal(true)
|
||||
},
|
||||
id: 'word_count',
|
||||
},
|
||||
{
|
||||
type: 'command',
|
||||
label: t('make_a_copy'),
|
||||
disabled: anonymous,
|
||||
handler: () => {
|
||||
setShowCloneProjectModal(true)
|
||||
},
|
||||
id: 'copy_project',
|
||||
},
|
||||
],
|
||||
[t, setView, view, wordCountEnabled, anonymous]
|
||||
)
|
||||
const fileMenuStructure: MenuStructure = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'file-file-tree',
|
||||
children: ['new_file', 'new_folder', 'upload_file', 'copy_project'],
|
||||
},
|
||||
{ id: 'file-tools', children: ['show_version_history', 'word_count'] },
|
||||
{ id: 'submit', children: ['submit-project', 'manage-template'] },
|
||||
{
|
||||
id: 'file-download',
|
||||
children: ['download-as-source-zip', 'download-pdf'],
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
children: ['open-settings'],
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const editMenuStructure: MenuStructure = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'edit-undo-redo',
|
||||
children: ['undo', 'redo'],
|
||||
},
|
||||
{
|
||||
id: 'edit-search',
|
||||
children: ['find', 'select-all'],
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const insertMenuStructure: MenuStructure = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'insert-latex',
|
||||
children: [
|
||||
{
|
||||
id: 'insert-math-group',
|
||||
title: t('math'),
|
||||
children: ['insert-inline-math', 'insert-display-math'],
|
||||
},
|
||||
'insert-symbol',
|
||||
{
|
||||
id: 'insert-figure-group',
|
||||
title: t('figure'),
|
||||
children: [
|
||||
'insert-figure-from-computer',
|
||||
'insert-figure-from-project-files',
|
||||
'insert-figure-from-another-project',
|
||||
'insert-figure-from-url',
|
||||
],
|
||||
},
|
||||
'insert-table',
|
||||
'insert-citation',
|
||||
'insert-link',
|
||||
'insert-cross-reference',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'insert-comment',
|
||||
children: ['comment'],
|
||||
},
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
const formatMenuStructure: MenuStructure = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'format-text',
|
||||
children: ['format-bold', 'format-italics'],
|
||||
},
|
||||
{
|
||||
id: 'format-list',
|
||||
children: [
|
||||
'format-bullet-list',
|
||||
'format-numbered-list',
|
||||
'format-increase-indentation',
|
||||
'format-decrease-indentation',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'format-paragraph',
|
||||
title: t('paragraph_styles'),
|
||||
children: [
|
||||
'format-style-normal',
|
||||
'format-style-section',
|
||||
'format-style-subsection',
|
||||
'format-style-subsubsection',
|
||||
'format-style-paragraph',
|
||||
'format-style-subparagraph',
|
||||
],
|
||||
},
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
const pdfControlsMenuSectionStructure: MenuSectionStructure = useMemo(
|
||||
() => ({
|
||||
title: t('pdf_preview'),
|
||||
id: 'pdf-controls',
|
||||
children: [
|
||||
'view-pdf-presentation-mode',
|
||||
'view-pdf-zoom-in',
|
||||
'view-pdf-zoom-out',
|
||||
'view-pdf-fit-width',
|
||||
'view-pdf-fit-height',
|
||||
],
|
||||
}),
|
||||
[t]
|
||||
)
|
||||
|
||||
const { mathPreview, setMathPreview, breadcrumbs, setBreadcrumbs } =
|
||||
useProjectSettingsContext()
|
||||
|
||||
const toggleMathPreview = useCallback(() => {
|
||||
setMathPreview(!mathPreview)
|
||||
}, [setMathPreview, mathPreview])
|
||||
|
||||
const toggleBreadcrumbs = useCallback(() => {
|
||||
setBreadcrumbs(!breadcrumbs)
|
||||
}, [setBreadcrumbs, breadcrumbs])
|
||||
|
||||
const { setActiveModal } = useRailContext()
|
||||
const openKeyboardShortcutsModal = useCallback(() => {
|
||||
setActiveModal('keyboard-shortcuts')
|
||||
}, [setActiveModal])
|
||||
const openContactUsModal = useCallback(() => {
|
||||
setActiveModal('contact-us')
|
||||
}, [setActiveModal])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuBar
|
||||
className="ide-redesign-toolbar-menu-bar"
|
||||
id="toolbar-menu-bar-item"
|
||||
>
|
||||
<CommandDropdown menu={fileMenuStructure} title={t('file')} id="file" />
|
||||
<CommandDropdown menu={editMenuStructure} title={t('edit')} id="edit" />
|
||||
<CommandDropdown
|
||||
menu={insertMenuStructure}
|
||||
title={t('insert')}
|
||||
id="insert"
|
||||
/>
|
||||
<MenuBarDropdown
|
||||
title={t('view')}
|
||||
id="view"
|
||||
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
|
||||
>
|
||||
<ChangeLayoutOptions />
|
||||
<DropdownDivider />
|
||||
<DropdownHeader>Editor settings</DropdownHeader>
|
||||
<MenuBarOption
|
||||
eventKey="show_breadcrumbs"
|
||||
title={t('show_breadcrumbs')}
|
||||
leadingIcon={
|
||||
breadcrumbs ? 'check' : <DropdownItem.EmptyLeadingIcon />
|
||||
}
|
||||
onClick={toggleBreadcrumbs}
|
||||
/>
|
||||
<MenuBarOption
|
||||
eventKey="show_equation_preview"
|
||||
title={t('show_equation_preview')}
|
||||
leadingIcon={
|
||||
mathPreview ? 'check' : <DropdownItem.EmptyLeadingIcon />
|
||||
}
|
||||
onClick={toggleMathPreview}
|
||||
/>
|
||||
<CommandSection
|
||||
section={pdfControlsMenuSectionStructure}
|
||||
includeDivider
|
||||
/>
|
||||
</MenuBarDropdown>
|
||||
<CommandDropdown
|
||||
menu={formatMenuStructure}
|
||||
title={t('format')}
|
||||
id="format"
|
||||
/>
|
||||
<MenuBarDropdown
|
||||
title={t('help')}
|
||||
id="help"
|
||||
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
|
||||
>
|
||||
<MenuBarOption
|
||||
eventKey="keyboard_shortcuts"
|
||||
title={t('keyboard_shortcuts')}
|
||||
onClick={openKeyboardShortcutsModal}
|
||||
/>
|
||||
<MenuBarOption
|
||||
title={t('documentation')}
|
||||
eventKey="documentation"
|
||||
href="/learn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
<DropdownDivider />
|
||||
<MenuBarOption
|
||||
eventKey="contact_us"
|
||||
title={t('contact_us')}
|
||||
onClick={openContactUsModal}
|
||||
/>
|
||||
</MenuBarDropdown>
|
||||
</MenuBar>
|
||||
<WordCountModal
|
||||
show={showWordCountModal}
|
||||
handleHide={() => setShowWordCountModal(false)}
|
||||
/>
|
||||
<EditorCloneProjectModalWrapper
|
||||
show={showCloneProjectModal}
|
||||
handleHide={() => setShowCloneProjectModal(false)}
|
||||
openProject={openProject}
|
||||
/>
|
||||
{menubarExtraComponents.map(
|
||||
({ import: { default: Component } }, index) => (
|
||||
<Component key={index} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import {
|
||||
OnlineUser,
|
||||
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'
|
||||
|
||||
export const OnlineUsers = () => {
|
||||
const { openDoc } = useEditorManagerContext()
|
||||
const { onlineUsersArray } = useOnlineUsersContext()
|
||||
|
||||
const goToUser = useCallback(
|
||||
async (user: OnlineUser) => {
|
||||
if (user.doc && typeof user.row === 'number') {
|
||||
return await openDoc(user.doc, { gotoLine: user.row + 1 })
|
||||
}
|
||||
},
|
||||
[openDoc]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="ide-redesign-online-users">
|
||||
<OnlineUsersWidget onlineUsers={onlineUsersArray} goToUser={goToUser} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownDivider,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { DownloadProjectPDF, DownloadProjectZip } from './download-project'
|
||||
import { useCallback, useState } from 'react'
|
||||
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
|
||||
import EditableLabel from './editable-label'
|
||||
import { DuplicateProject } from './duplicate-project'
|
||||
|
||||
const [publishModalModules] = importOverleafModules('publishModal')
|
||||
const SubmitProjectButton = publishModalModules?.import.NewPublishDropdownButton
|
||||
|
||||
export const ToolbarProjectTitle = () => {
|
||||
const { cobranding } = useEditorContext()
|
||||
const { t } = useTranslation()
|
||||
const { renameProject } = useEditorContext()
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { name } = useProjectContext()
|
||||
const shouldDisplaySubmitButton =
|
||||
(permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&
|
||||
SubmitProjectButton
|
||||
const hasRenamePermissions = permissionsLevel === 'owner'
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const onRename = useCallback(
|
||||
(name: string) => {
|
||||
if (name) {
|
||||
renameProject(name)
|
||||
}
|
||||
setIsRenaming(false)
|
||||
},
|
||||
[renameProject]
|
||||
)
|
||||
const onCancel = useCallback(() => {
|
||||
setIsRenaming(false)
|
||||
}, [])
|
||||
|
||||
if (isRenaming) {
|
||||
return (
|
||||
<EditableLabel
|
||||
onChange={onRename}
|
||||
onCancel={onCancel}
|
||||
initialText={name}
|
||||
maxLength={150}
|
||||
className="ide-redesign-toolbar-editable-project-name"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown align="start" className="ide-redesign-toolbar-project-dropdown">
|
||||
<DropdownToggle
|
||||
id="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')}
|
||||
/>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu renderOnMount>
|
||||
{shouldDisplaySubmitButton && !cobranding && (
|
||||
<>
|
||||
<SubmitProjectButton />
|
||||
<DropdownDivider />
|
||||
</>
|
||||
)}
|
||||
<DownloadProjectPDF />
|
||||
<DownloadProjectZip />
|
||||
<DropdownDivider />
|
||||
<DuplicateProject />
|
||||
<OLDropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsRenaming(true)
|
||||
}}
|
||||
disabled={!hasRenamePermissions}
|
||||
>
|
||||
{t('rename')}
|
||||
</OLDropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import ShareProjectModal from '@/features/share-project-modal/components/share-project-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
|
||||
export default function ShareProjectButton() {
|
||||
const { t } = useTranslation()
|
||||
const { sendEventOnce } = useEditorAnalytics()
|
||||
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
|
||||
const handleOpenShareModal = useCallback(() => {
|
||||
sendEventOnce('ide-open-share-modal-once')
|
||||
setShowShareModal(true)
|
||||
}, [sendEventOnce])
|
||||
|
||||
const handleHideShareModal = useCallback(() => {
|
||||
setShowShareModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ide-redesign-toolbar-button-container">
|
||||
<OLButton
|
||||
size="sm"
|
||||
variant="primary"
|
||||
leadingIcon={<MaterialIcon type="person_add" />}
|
||||
onClick={handleOpenShareModal}
|
||||
>
|
||||
{t('share')}
|
||||
</OLButton>
|
||||
</div>
|
||||
<ShareProjectModal
|
||||
show={showShareModal}
|
||||
handleOpen={handleOpenShareModal}
|
||||
handleHide={handleHideShareModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLIconButton from '@/shared/components/ol/ol-icon-button'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { useCallback } from 'react'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
|
||||
export default function ShowHistoryButton() {
|
||||
const { t } = useTranslation()
|
||||
const { view, setView, restoreView } = useLayoutContext()
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
|
||||
const toggleHistoryOpen = useCallback(() => {
|
||||
const action = view === 'history' ? 'close' : 'open'
|
||||
sendEvent('navigation-clicked-history', { action })
|
||||
if (view === 'history') {
|
||||
restoreView()
|
||||
} else {
|
||||
setView('history')
|
||||
}
|
||||
}, [view, setView, sendEvent, restoreView])
|
||||
|
||||
return (
|
||||
<div className="ide-redesign-toolbar-button-container">
|
||||
<OLTooltip
|
||||
id="tooltip-open-history"
|
||||
description={t('history')}
|
||||
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||
>
|
||||
<OLIconButton
|
||||
icon="history"
|
||||
className="ide-redesign-toolbar-button-subdued ide-redesign-toolbar-button-icon"
|
||||
onClick={toggleHistoryOpen}
|
||||
accessibilityLabel={t('history')}
|
||||
/>
|
||||
</OLTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ToolbarMenuBar } from './menu-bar'
|
||||
import { ToolbarProjectTitle } from './project-title'
|
||||
import { OnlineUsers } from './online-users'
|
||||
import ShareProjectButton from './share-project-button'
|
||||
import ChangeLayoutButton from './change-layout-button'
|
||||
import ShowHistoryButton from './show-history-button'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import BackToEditorButton from '@/features/editor-navigation-toolbar/components/back-to-editor-button'
|
||||
import { useCallback } from 'react'
|
||||
import * as eventTracking from '../../../../infrastructure/event-tracking'
|
||||
import { ToolbarLogos } from './logos'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import UpgradeButton from './upgrade-button'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
|
||||
const [publishModalModules] = importOverleafModules('publishModal')
|
||||
const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
|
||||
|
||||
export const Toolbar = () => {
|
||||
const { view, restoreView } = useLayoutContext()
|
||||
const { cobranding, isRestrictedTokenMember } = useEditorContext()
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { t } = useTranslation()
|
||||
const shouldDisplaySubmitButton =
|
||||
(permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&
|
||||
SubmitProjectButton
|
||||
|
||||
const handleBackToEditorClick = useCallback(() => {
|
||||
eventTracking.sendMB('navigation-clicked-history', { action: 'close' })
|
||||
restoreView()
|
||||
}, [restoreView])
|
||||
|
||||
if (view === 'history') {
|
||||
return (
|
||||
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
|
||||
<div className="d-flex align-items-center">
|
||||
<BackToEditorButton onClick={handleBackToEditorClick} />
|
||||
</div>
|
||||
<ToolbarProjectTitle />
|
||||
<div /> {/* Empty div used for spacing */}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
|
||||
<div className="ide-redesign-toolbar-menu">
|
||||
<ToolbarLogos cobranding={cobranding} />
|
||||
<ToolbarMenuBar />
|
||||
</div>
|
||||
<ToolbarProjectTitle />
|
||||
<div className="ide-redesign-toolbar-actions">
|
||||
<OnlineUsers />
|
||||
{!isRestrictedTokenMember && <ShowHistoryButton />}
|
||||
<ChangeLayoutButton />
|
||||
{shouldDisplaySubmitButton && cobranding && (
|
||||
<SubmitProjectButton cobranding={cobranding} />
|
||||
)}
|
||||
<ShareProjectButton />
|
||||
{getMeta('ol-showUpgradePrompt') && <UpgradeButton />}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as eventTracking from '../../../../infrastructure/event-tracking'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
|
||||
export default function UpgradeButton() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
function handleClick() {
|
||||
eventTracking.send('subscription-funnel', 'code-editor', 'upgrade')
|
||||
eventTracking.sendMB('upgrade-button-click', { source: 'code-editor' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ide-redesign-toolbar-button-container">
|
||||
<OLButton
|
||||
variant="premium"
|
||||
size="sm"
|
||||
href="/user/subscription/plans?itm_referrer=editor-header-upgrade-prompt"
|
||||
target="_blank"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{t('upgrade')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user