first commit
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -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'
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
@@ -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>
|
||||
)
|
||||
}
|
@@ -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>
|
||||
)
|
||||
}
|
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -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>
|
||||
)
|
||||
}
|
@@ -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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user