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,45 @@
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import {
Dropdown,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import React, { forwardRef } from 'react'
import ChangeLayoutOptions from './change-layout-options'
const LayoutDropdownToggleButton = forwardRef<
HTMLButtonElement,
{
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
}
>(({ onClick }, ref) => {
return (
<OLButton
size="sm"
className="ide-redesign-toolbar-button-subdued"
ref={ref}
onClick={onClick}
leadingIcon={<MaterialIcon unfilled type="space_dashboard" />}
/>
)
})
LayoutDropdownToggleButton.displayName = 'LayoutDropdownToggleButton'
export default function ChangeLayoutButton() {
return (
<div className="ide-redesign-toolbar-button-container">
<Dropdown className="toolbar-item layout-dropdown" align="end">
<DropdownToggle
id="layout-dropdown-btn"
className="btn-full-height"
as={LayoutDropdownToggleButton}
/>
<DropdownMenu>
<ChangeLayoutOptions />
</DropdownMenu>
</Dropdown>
</div>
)
}

View File

@@ -0,0 +1,175 @@
import {
DropdownItem,
DropdownHeader,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import {
IdeLayout,
IdeView,
useLayoutContext,
} from '@/shared/context/layout-context'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import useEventListener from '@/shared/hooks/use-event-listener'
import { DetachRole } from '@/shared/context/detach-context'
import { Spinner } from 'react-bootstrap-5'
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,
onClick,
children,
}: {
active: boolean
leadingIcon: string
onClick: () => void
children: React.ReactNode
processing?: boolean
disabled?: boolean
}) => {
let trailingIcon: string | React.ReactNode | null = null
if (processing) {
trailingIcon = (
<Spinner animation="border" aria-hidden="true" size="sm" role="status" />
)
} else if (active) {
trailingIcon = 'check'
}
return (
<DropdownItem
active={active}
aria-current={active}
trailingIcon={trailingIcon}
disabled={disabled}
onClick={onClick}
leadingIcon={leadingIcon}
>
{children}
</DropdownItem>
)
}
export default function ChangeLayoutOptions() {
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]
)
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"
>
{t('split_view')}
</LayoutDropdownItem>
<LayoutDropdownItem
onClick={() => handleChangeLayout('flat', 'editor')}
active={activeLayoutOption === 'editorOnly'}
leadingIcon="edit"
>
{t('editor_only')}
</LayoutDropdownItem>
<LayoutDropdownItem
onClick={() => handleChangeLayout('flat', 'pdf')}
active={activeLayoutOption === 'pdfOnly'}
leadingIcon="picture_as_pdf"
>
{t('pdf_only')}
</LayoutDropdownItem>
<LayoutDropdownItem
onClick={() => handleDetach()}
active={activeLayoutOption === 'detachedPdf' && detachIsLinked}
disabled={!detachable}
leadingIcon="open_in_new"
processing={waitingForDetachedLink}
>
{t('open_pdf_in_separate_tab')}
</LayoutDropdownItem>
</>
)
}

View File

@@ -0,0 +1,158 @@
import {
Command,
useCommandRegistry,
} from '@/features/ide-react/context/command-registry-context'
import {
DropdownDivider,
DropdownHeader,
} from '@/features/ui/components/bootstrap-5/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' }
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 } = useCommandRegistry()
const populatedSections = useMemo(
() =>
menu
.map(section => populateSectionOrGroup(section, registry))
.filter(x => x.children.length > 0),
[menu, registry]
)
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}>
{index > 0 && <DropdownDivider />}
{section.title && <DropdownHeader>{section.title}</DropdownHeader>}
{section.children.map(child => (
<CommandDropdownChild item={child} key={child.id} />
))}
</Fragment>
)
})}
</MenuBarDropdown>
)
}
export const CommandSection = ({
section: sectionStructure,
}: {
section: MenuSectionStructure<CommandId>
}) => {
const { registry } = useCommandRegistry()
const section = populateSectionOrGroup(sectionStructure, registry)
if (section.children.length === 0) {
return null
}
return (
<>
{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
key={item.id}
title={item.label}
// eslint-disable-next-line react/jsx-handler-names
onClick={onClickHandler}
href={item.href}
disabled={item.disabled}
/>
)
} 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>
): 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)
if (populatedChild.children.length === 0) {
// Skip empty groups
return undefined
}
return populatedChild
}
const command = registry.get(child)
if (command) {
return { ...command, 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'
}

View File

@@ -0,0 +1,100 @@
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
import OLTooltip from '@/features/ui/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'
export const DownloadProjectZip = () => {
const { t } = useTranslation()
const { _id: 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 { _id: projectId } = useProjectContext()
const sendDownloadEvent = useCallback(() => {
sendMB('download-pdf-button-click', {
projectId,
location: 'project-name-dropdown',
isSmallDevice,
})
}, [projectId])
useCommandProvider(
() => [
{
id: 'download-pdf',
disabled: !pdfUrl,
href: pdfDownloadUrl || pdfUrl,
handler: ({ location }) => {
sendMB('download-pdf-button-click', {
projectId,
location,
isSmallDevice,
})
},
label: t('download_as_pdf'),
},
],
[t, pdfUrl, projectId, pdfDownloadUrl]
)
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
}
}

View File

@@ -0,0 +1,70 @@
import OLFormControl from '@/features/ui/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

View File

@@ -0,0 +1,47 @@
import { useIdeRedesignSwitcherContext } from '@/features/ide-react/context/ide-redesign-switcher-context'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon from '@/shared/components/material-icon'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
export const LabsActions = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const openEditorRedesignSwitcherModal = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
return (
<>
<div className="ide-redesign-toolbar-button-container">
<OLTooltip
id="tooltip-labs-button"
description={t(
'this_is_a_labs_experiment_for_the_new_overleaf_editor_some_features_are_still_in_progress'
)}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<OLButton
size="sm"
variant="info"
className="ide-redesign-labs-button"
onClick={openEditorRedesignSwitcherModal}
leadingIcon={<MaterialIcon type="experiment" unfilled />}
>
{t('labs')}
</OLButton>
</OLTooltip>
</div>
<div className="ide-redesign-toolbar-button-container">
<a
href="https://forms.gle/soyVStc5qDx9na1Z6"
rel="noopener noreferrer"
target="_blank"
className="ide-redesign-toolbar-labs-feedback-link"
>
{t('give_feedback')}
</a>
</div>
</>
)
}

View File

@@ -0,0 +1,268 @@
import {
DropdownDivider,
DropdownHeader,
} from '@/features/ui/components/bootstrap-5/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 { MouseEventHandler, useCallback, useMemo } from 'react'
import { useIdeRedesignSwitcherContext } from '@/features/ide-react/context/ide-redesign-switcher-context'
import { useSwitchEnableNewEditorState } from '../../hooks/use-switch-enable-new-editor-state'
import MaterialIcon from '@/shared/components/material-icon'
import OLSpinner from '@/features/ui/components/ol/ol-spinner'
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 { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useRailContext } from '../../contexts/rail-context'
export const ToolbarMenuBar = () => {
const { t } = useTranslation()
const { setShowSwitcherModal } = useIdeRedesignSwitcherContext()
const openEditorRedesignSwitcherModal = useCallback(() => {
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
const { setView, view } = useLayoutContext()
useCommandProvider(
() => [
{
type: 'command',
label: t('show_version_history'),
handler: () => {
setView(view === 'history' ? 'editor' : 'history')
},
id: 'show_version_history',
},
],
[t, setView, view]
)
const fileMenuStructure: MenuStructure = useMemo(
() => [
{
id: 'file-file-tree',
children: ['new_file', 'new_folder', 'upload_file'],
},
{ id: 'file-history', children: ['show_version_history'] },
{
id: 'file-download',
children: ['download-as-source-zip', 'download-pdf'],
},
],
[]
)
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 {
userSettings: { mathPreview },
setUserSettings,
} = useUserSettingsContext()
const toggleMathPreview = useCallback(() => {
setUserSettings(prev => {
return {
...prev,
mathPreview: !prev.mathPreview,
}
})
}, [setUserSettings])
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 />
<DropdownHeader>Editor settings</DropdownHeader>
<MenuBarOption
title={t('show_equation_preview')}
trailingIcon={mathPreview ? 'check' : undefined}
onClick={toggleMathPreview}
/>
<CommandSection section={pdfControlsMenuSectionStructure} />
</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
title={t('keyboard_shortcuts')}
onClick={openKeyboardShortcutsModal}
/>
<MenuBarOption
title={t('documentation')}
href="/learn"
target="_blank"
rel="noopener noreferrer"
/>
<DropdownDivider />
<MenuBarOption title={t('contact_us')} onClick={openContactUsModal} />
<MenuBarOption
title={t('give_feedback')}
href="https://forms.gle/soyVStc5qDx9na1Z6"
target="_blank"
rel="noopener noreferrer"
/>
<DropdownDivider />
<SwitchToOldEditorMenuBarOption />
<MenuBarOption
title="What's new?"
onClick={openEditorRedesignSwitcherModal}
/>
</MenuBarDropdown>
</MenuBar>
)
}
const SwitchToOldEditorMenuBarOption = () => {
const { loading, error, setEditorRedesignStatus } =
useSwitchEnableNewEditorState()
const disable: MouseEventHandler = useCallback(
event => {
// Don't close the dropdown
event.stopPropagation()
setEditorRedesignStatus(false)
},
[setEditorRedesignStatus]
)
let icon = null
if (loading) {
icon = <OLSpinner size="sm" />
} else if (error) {
icon = <MaterialIcon type="error" title={error} className="text-danger" />
}
return (
<MenuBarOption
title="Switch to old editor"
onClick={disable}
disabled={loading}
trailingIcon={icon}
/>
)
}

View File

@@ -0,0 +1,27 @@
import OnlineUsersWidget from '@/features/editor-navigation-toolbar/components/online-users-widget'
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'
export const OnlineUsers = () => {
const { openDoc } = useEditorManagerContext()
const { onlineUsersArray } = useOnlineUsersContext()
const goToUser = useCallback(
(user: OnlineUser) => {
if (user.doc && typeof user.row === 'number') {
openDoc(user.doc, { gotoLine: user.row + 1 })
}
},
[openDoc]
)
return (
<div className="ide-redesign-online-users">
<OnlineUsersWidget onlineUsers={onlineUsersArray} goToUser={goToUser} />
</div>
)
}

View File

@@ -0,0 +1,87 @@
import {
Dropdown,
DropdownDivider,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/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 { DownloadProjectPDF, DownloadProjectZip } from './download-project'
import { useCallback, useState } from 'react'
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
import EditableLabel from './editable-label'
const [publishModalModules] = importOverleafModules('publishModal')
const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
export const ToolbarProjectTitle = () => {
const { t } = useTranslation()
const { permissionsLevel, renameProject } = useEditorContext()
const { name } = useProjectContext()
const shouldDisplaySubmitButton =
(permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&
SubmitProjectButton
const hasRenamePermissions = permissionsLevel === 'owner'
const [isRenaming, setIsRenaming] = useState(false)
const onRename = useCallback(
name => {
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">
<DropdownToggle
id="project-title-options"
className="ide-redesign-toolbar-dropdown-toggle-subdued fw-bold ide-redesign-toolbar-button-subdued"
>
{name}
<MaterialIcon
type="keyboard_arrow_down"
accessibilityLabel={t('project_title_options')}
/>
</DropdownToggle>
<DropdownMenu renderOnMount>
{shouldDisplaySubmitButton && (
<>
<SubmitProjectButton />
<DropdownDivider />
</>
)}
<DownloadProjectPDF />
<DownloadProjectZip />
<DropdownDivider />
<OLDropdownMenuItem
onClick={() => {
setIsRenaming(true)
}}
disabled={!hasRenamePermissions}
>
{t('rename')}
</OLDropdownMenuItem>
</DropdownMenu>
</Dropdown>
)
}

View File

@@ -0,0 +1,41 @@
import ShareProjectModal from '@/features/share-project-modal/components/share-project-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import * as eventTracking from '@/infrastructure/event-tracking'
export default function ShareProjectButton() {
const { t } = useTranslation()
const [showShareModal, setShowShareModal] = useState(false)
const handleOpenShareModal = useCallback(() => {
eventTracking.sendMBOnce('ide-open-share-modal-once')
setShowShareModal(true)
}, [])
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}
/>
</>
)
}

View File

@@ -0,0 +1,37 @@
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import { useTranslation } from 'react-i18next'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useCallback } from 'react'
export default function ShowHistoryButton() {
const { t } = useTranslation()
const { view, setView } = useLayoutContext()
const toggleHistoryOpen = useCallback(() => {
const action = view === 'history' ? 'close' : 'open'
eventTracking.sendMB('navigation-clicked-history', { action })
setView(view === 'history' ? 'editor' : 'history')
}, [view, setView])
return (
<div className="ide-redesign-toolbar-button-container">
<OLTooltip
id="tooltip-open-history"
description={t('history')}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<OLButton
size="sm"
variant="ghost"
className="ide-redesign-toolbar-button-subdued"
leadingIcon={<MaterialIcon type="history" />}
onClick={toggleHistoryOpen}
/>
</OLTooltip>
</div>
)
}

View File

@@ -0,0 +1,69 @@
import MaterialIcon from '@/shared/components/material-icon'
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 { LabsActions } from './labs-actions'
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'
export const Toolbar = () => {
const { view, setView } = useLayoutContext()
const handleBackToEditorClick = useCallback(() => {
eventTracking.sendMB('navigation-clicked-history', { action: 'close' })
setView('editor')
}, [setView])
if (view === 'history') {
return (
<div className="ide-redesign-toolbar">
<div className="d-flex align-items-center">
<BackToEditorButton onClick={handleBackToEditorClick} />
</div>
<ToolbarProjectTitle />
<div /> {/* Empty div used for spacing */}
</div>
)
}
return (
<div className="ide-redesign-toolbar">
<ToolbarMenus />
<ToolbarProjectTitle />
<ToolbarButtons />
</div>
)
}
const ToolbarMenus = () => {
const { t } = useTranslation()
return (
<div className="ide-redesign-toolbar-menu">
<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>
<ToolbarMenuBar />
</div>
)
}
const ToolbarButtons = () => {
return (
<div className="ide-redesign-toolbar-actions">
<LabsActions />
<OnlineUsers />
<ShowHistoryButton />
<ChangeLayoutButton />
<ShareProjectButton />
</div>
)
}