mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +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
@@ -0,0 +1,98 @@
|
||||
import MaterialIcon, {
|
||||
AvailableUnfilledIcon,
|
||||
} from '@/shared/components/material-icon'
|
||||
import { forwardRef, ReactElement, useCallback } from 'react'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import { DropdownToggle as BS5DropdownToggle } from 'react-bootstrap'
|
||||
|
||||
type RailActionButton = {
|
||||
key: string
|
||||
icon: AvailableUnfilledIcon
|
||||
title: string
|
||||
action: () => void
|
||||
hide?: boolean
|
||||
ref?: React.Ref<HTMLButtonElement>
|
||||
}
|
||||
|
||||
type RailDropdown = {
|
||||
key: string
|
||||
icon: AvailableUnfilledIcon
|
||||
title: string
|
||||
dropdown: ReactElement
|
||||
hide?: boolean
|
||||
ref?: React.Ref<HTMLButtonElement>
|
||||
}
|
||||
|
||||
export type RailAction = RailDropdown | RailActionButton
|
||||
|
||||
const RailActionElement = forwardRef<HTMLButtonElement, { action: RailAction }>(
|
||||
({ action }, ref) => {
|
||||
const onActionClick = useCallback(() => {
|
||||
if ('action' in action) {
|
||||
action.action()
|
||||
}
|
||||
}, [action])
|
||||
|
||||
if (action.hide) {
|
||||
return null
|
||||
}
|
||||
|
||||
if ('dropdown' in action) {
|
||||
return (
|
||||
<Dropdown align="end" drop="end">
|
||||
<OLTooltip
|
||||
id={`rail-dropdown-tooltip-${action.key}`}
|
||||
description={action.title}
|
||||
overlayProps={{ delay: 0, placement: 'right' }}
|
||||
>
|
||||
<span>
|
||||
<DropdownToggle
|
||||
ref={ref as React.ForwardedRef<typeof BS5DropdownToggle>}
|
||||
id={`rail-dropdown-btn-${action.key}`}
|
||||
className="ide-rail-tab-link ide-rail-tab-button ide-rail-tab-dropdown"
|
||||
as="button"
|
||||
aria-label={action.title}
|
||||
>
|
||||
<MaterialIcon
|
||||
className="ide-rail-tab-link-icon"
|
||||
type={action.icon}
|
||||
unfilled
|
||||
/>
|
||||
</DropdownToggle>
|
||||
</span>
|
||||
</OLTooltip>
|
||||
{action.dropdown}
|
||||
</Dropdown>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<OLTooltip
|
||||
id={`rail-tab-tooltip-${action.key}`}
|
||||
description={action.title}
|
||||
overlayProps={{ delay: 0, placement: 'right' }}
|
||||
>
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={onActionClick}
|
||||
className="ide-rail-tab-link ide-rail-tab-button"
|
||||
aria-label={action.title}
|
||||
>
|
||||
<MaterialIcon
|
||||
className="ide-rail-tab-link-icon"
|
||||
type={action.icon}
|
||||
unfilled
|
||||
/>
|
||||
</button>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
RailActionElement.displayName = 'RailActionElement'
|
||||
|
||||
export default RailActionElement
|
||||
@@ -0,0 +1,43 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRailContext } from '@/features/ide-react/context/rail-context'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
DropdownDivider,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
|
||||
export default function RailHelpDropdown() {
|
||||
const showSupport = getMeta('ol-showSupport')
|
||||
const { t } = useTranslation()
|
||||
const { setActiveModal } = useRailContext()
|
||||
const openKeyboardShortcutsModal = useCallback(() => {
|
||||
setActiveModal('keyboard-shortcuts')
|
||||
}, [setActiveModal])
|
||||
const openContactUsModal = useCallback(() => {
|
||||
setActiveModal('contact-us')
|
||||
}, [setActiveModal])
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownItem onClick={openKeyboardShortcutsModal}>
|
||||
{t('keyboard_shortcuts')}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
href="/learn"
|
||||
role="menuitem"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('documentation')}
|
||||
</DropdownItem>
|
||||
<DropdownDivider />
|
||||
{showSupport && (
|
||||
<DropdownItem onClick={openContactUsModal}>
|
||||
{t('contact_us')}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import OLBadge from '@/shared/components/ol/ol-badge'
|
||||
|
||||
type RailIndicatorProps = {
|
||||
type: 'danger' | 'warning' | 'info'
|
||||
count: number
|
||||
}
|
||||
|
||||
function formatNumber(num: number) {
|
||||
if (num > 99) {
|
||||
return '99+'
|
||||
}
|
||||
return Math.floor(num).toString()
|
||||
}
|
||||
|
||||
export const RailIndicator = ({ count, type }: RailIndicatorProps) => {
|
||||
return <OLBadge bg={type}>{formatNumber(count)}</OLBadge>
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FC } from 'react'
|
||||
import {
|
||||
RailModalKey,
|
||||
useRailContext,
|
||||
} from '@/features/ide-react/context/rail-context'
|
||||
import { RailHelpContactUsModal } from '@/features/ide-redesign/components/help/contact-us'
|
||||
import { RailHelpShowHotkeysModal } from '@/features/ide-redesign/components/help/keyboard-shortcuts'
|
||||
import DictionarySettingsModal from '@/features/ide-redesign/components/settings/editor-settings/dictionary-settings-modal'
|
||||
|
||||
const RAIL_MODALS: {
|
||||
key: RailModalKey
|
||||
modalComponentFunction: FC<{ show: boolean }>
|
||||
}[] = [
|
||||
{
|
||||
key: 'keyboard-shortcuts',
|
||||
modalComponentFunction: RailHelpShowHotkeysModal,
|
||||
},
|
||||
{
|
||||
key: 'contact-us',
|
||||
modalComponentFunction: RailHelpContactUsModal,
|
||||
},
|
||||
{
|
||||
key: 'dictionary',
|
||||
modalComponentFunction: DictionarySettingsModal,
|
||||
},
|
||||
]
|
||||
|
||||
export default function RailModals() {
|
||||
const { activeModal } = useRailContext()
|
||||
|
||||
return RAIL_MODALS.map(({ key, modalComponentFunction: Component }) => (
|
||||
<Component key={key} show={activeModal === key} />
|
||||
))
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { DropdownMenu } from '@/shared/components/dropdown/dropdown-menu'
|
||||
import { RailTabKey } from '@/features/ide-react/context/rail-context'
|
||||
import { RailElement } from '@/features/ide-react/util/rail-types'
|
||||
import RailTab from './rail-tab'
|
||||
import { shouldIncludeElement } from '@/features/ide-react/util/rail-utils'
|
||||
|
||||
export default function RailOverflowDropdown({
|
||||
tabs,
|
||||
isOpen,
|
||||
selectedTab,
|
||||
}: {
|
||||
tabs: RailElement[]
|
||||
isOpen: boolean
|
||||
selectedTab: RailTabKey
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu className="ide-rail-overflow-dropdown">
|
||||
{tabs
|
||||
.filter(shouldIncludeElement)
|
||||
.map(({ icon, key, indicator, title, disabled }) => (
|
||||
<RailTab
|
||||
open={isOpen && selectedTab === key}
|
||||
key={key}
|
||||
eventKey={key}
|
||||
icon={icon}
|
||||
indicator={indicator}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRailContext } from '@/features/ide-react/context/rail-context'
|
||||
import OLIconButton from '@/shared/components/ol/ol-icon-button'
|
||||
import React, { useCallback } from 'react'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
|
||||
export default function RailPanelHeader({
|
||||
title,
|
||||
actions,
|
||||
onClose,
|
||||
}: {
|
||||
title: React.ReactNode
|
||||
actions?: React.ReactElement
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { handlePaneCollapse } = useRailContext()
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
handlePaneCollapse()
|
||||
if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
}, [handlePaneCollapse, onClose])
|
||||
|
||||
return (
|
||||
<div className="rail-panel-header">
|
||||
<h4 className="rail-panel-title">{title}</h4>
|
||||
|
||||
<div className="rail-panel-header-actions">
|
||||
{actions}
|
||||
<OLTooltip
|
||||
id="close-rail-panel"
|
||||
description={t('close')}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<OLIconButton
|
||||
onClick={handleClose}
|
||||
className="rail-panel-header-button-subdued"
|
||||
icon="close"
|
||||
accessibilityLabel={t('close')}
|
||||
size="sm"
|
||||
/>
|
||||
</OLTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Panel } from 'react-resizable-panels'
|
||||
import { useRailContext } from '@/features/ide-react/context/rail-context'
|
||||
import classNames from 'classnames'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import usePreviousValue from '@/shared/hooks/use-previous-value'
|
||||
import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
|
||||
import { Tab } from 'react-bootstrap'
|
||||
import { RailElement } from '@/features/ide-react/util/rail-types'
|
||||
import { shouldIncludeElement } from '@/features/ide-react/util/rail-utils'
|
||||
|
||||
export default function RailPanel({
|
||||
isReviewPanelOpen,
|
||||
isHistoryView,
|
||||
railTabs,
|
||||
}: {
|
||||
isReviewPanelOpen: boolean
|
||||
isHistoryView: boolean
|
||||
railTabs: RailElement[]
|
||||
}) {
|
||||
const { selectedTab, panelRef, handlePaneExpand, handlePaneCollapse } =
|
||||
useRailContext()
|
||||
|
||||
const prevTab = usePreviousValue(selectedTab)
|
||||
|
||||
const tabHasChanged = useMemo(() => {
|
||||
return prevTab !== selectedTab
|
||||
}, [prevTab, selectedTab])
|
||||
|
||||
const onCollapse = useCallback(() => {
|
||||
if (!tabHasChanged) {
|
||||
handlePaneCollapse()
|
||||
}
|
||||
}, [tabHasChanged, handlePaneCollapse])
|
||||
|
||||
return (
|
||||
<Panel
|
||||
id={`ide-redesign-sidebar-panel-${isHistoryView ? 'file-tree' : selectedTab}`}
|
||||
className={classNames({ hidden: isReviewPanelOpen })}
|
||||
order={1}
|
||||
defaultSize={selectedTab === 'workbench' ? 20 : 15}
|
||||
minSize={5}
|
||||
maxSize={80}
|
||||
ref={panelRef}
|
||||
collapsible
|
||||
onCollapse={onCollapse}
|
||||
onExpand={handlePaneExpand}
|
||||
>
|
||||
{isHistoryView && <HistorySidebar />}
|
||||
<div
|
||||
className={classNames('ide-rail-content', {
|
||||
hidden: isHistoryView,
|
||||
})}
|
||||
>
|
||||
<Tab.Content className="ide-rail-tab-content">
|
||||
{railTabs
|
||||
.filter(shouldIncludeElement)
|
||||
.map(({ key, component, mountOnFirstLoad }) => (
|
||||
<Tab.Pane
|
||||
eventKey={key}
|
||||
key={key}
|
||||
mountOnEnter={!mountOnFirstLoad}
|
||||
>
|
||||
{component}
|
||||
</Tab.Pane>
|
||||
))}
|
||||
</Tab.Content>
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
|
||||
import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
|
||||
import classNames from 'classnames'
|
||||
import { useRailContext } from '@/features/ide-react/context/rail-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RailResizeHandle({
|
||||
isReviewPanelOpen,
|
||||
}: {
|
||||
isReviewPanelOpen: boolean
|
||||
}) {
|
||||
const { isOpen, setIsOpen, togglePane, setResizing } = useRailContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<HorizontalResizeHandle
|
||||
className={classNames({ hidden: isReviewPanelOpen })}
|
||||
resizable
|
||||
hitAreaMargins={{ coarse: 0, fine: 0 }}
|
||||
onDoubleClick={togglePane}
|
||||
onDragging={setResizing}
|
||||
>
|
||||
<HorizontalToggler
|
||||
id="ide-redesign-sidebar-panel"
|
||||
togglerType="west"
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
tooltipWhenOpen={t('tooltip_hide_panel')}
|
||||
tooltipWhenClosed={t('tooltip_show_panel')}
|
||||
/>
|
||||
</HorizontalResizeHandle>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import classNames from 'classnames'
|
||||
import { ComponentProps, forwardRef, ReactElement } from 'react'
|
||||
import { NavLink } from 'react-bootstrap'
|
||||
import { RailElement } from '@/features/ide-react/util/rail-types'
|
||||
|
||||
const RailTab = forwardRef<
|
||||
HTMLAnchorElement,
|
||||
{
|
||||
icon: RailElement['icon']
|
||||
eventKey?: string
|
||||
open: boolean
|
||||
indicator?: ReactElement
|
||||
title: string
|
||||
} & ComponentProps<'button'>
|
||||
>(({ icon, eventKey, className, open, indicator, title, ...props }, ref) => {
|
||||
return (
|
||||
<OLTooltip
|
||||
id={`rail-tab-tooltip-${eventKey}`}
|
||||
description={title}
|
||||
overlayProps={{ delay: 0, placement: 'right' }}
|
||||
>
|
||||
<NavLink
|
||||
{...props}
|
||||
ref={ref}
|
||||
eventKey={eventKey}
|
||||
className={classNames('ide-rail-tab-link', className, {
|
||||
'open-rail': open,
|
||||
})}
|
||||
>
|
||||
<RailTabIcon icon={icon} title={title} open={open} />
|
||||
{indicator}
|
||||
</NavLink>
|
||||
</OLTooltip>
|
||||
)
|
||||
})
|
||||
|
||||
const RailTabIcon = ({
|
||||
icon,
|
||||
title,
|
||||
open,
|
||||
}: {
|
||||
icon: RailElement['icon']
|
||||
title: string
|
||||
open: boolean
|
||||
}) => {
|
||||
if (typeof icon === 'string') {
|
||||
return open ? (
|
||||
<MaterialIcon
|
||||
type={icon}
|
||||
className="ide-rail-tab-link-icon"
|
||||
accessibilityLabel={title}
|
||||
/>
|
||||
) : (
|
||||
<MaterialIcon
|
||||
type={icon}
|
||||
className="ide-rail-tab-link-icon"
|
||||
unfilled
|
||||
accessibilityLabel={title}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
const Component = icon
|
||||
return <Component open={open} title={title} />
|
||||
}
|
||||
}
|
||||
|
||||
RailTab.displayName = 'RailTab'
|
||||
|
||||
export default RailTab
|
||||
@@ -0,0 +1,327 @@
|
||||
import { FC, RefObject, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Nav, TabContainer } from 'react-bootstrap'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import {
|
||||
RailTabKey,
|
||||
useRailContext,
|
||||
} from '@/features/ide-react/context/rail-context'
|
||||
import FileTreeOutlinePanel from '@/features/ide-redesign/components/file-tree/file-tree-outline-panel'
|
||||
import {
|
||||
ChatIndicator,
|
||||
ChatPane,
|
||||
} from '@/features/ide-redesign/components/chat/chat'
|
||||
import getMeta from '@/utils/meta'
|
||||
import classNames from 'classnames'
|
||||
import IntegrationsPanel from '@/features/ide-redesign/components/integrations-panel/integrations-panel'
|
||||
import { useChatContext } from '@/features/chat/context/chat-context'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
import {
|
||||
FullProjectSearchPanel,
|
||||
hasFullProjectSearch,
|
||||
} from '@/features/ide-redesign/components/full-project-search-panel'
|
||||
import { sendSearchEvent } from '@/features/event-tracking/search-events'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
|
||||
import RailHelpDropdown from './rail-help-dropdown'
|
||||
import RailTab from './rail-tab'
|
||||
import RailActionElement, { RailAction } from './rail-action-element'
|
||||
import { RailElement } from '@/features/ide-react/util/rail-types'
|
||||
import RailPanel from './rail-panel'
|
||||
import RailResizeHandle from './rail-resize-handle'
|
||||
import RailModals from './rail-modals'
|
||||
import RailOverflowDropdown from './rail-overflow-dropdown'
|
||||
import useRailOverflow from '@/features/ide-redesign/hooks/use-rail-overflow'
|
||||
import EditorTourRailTooltip from '@/features/ide-redesign/components/editor-tour/editor-tour-rail-tooltip'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import EditorTourThemeTooltip from '@/features/ide-redesign/components/editor-tour/editor-tour-theme-tooltip'
|
||||
import EditorTourGotQuestionsTooltip from '@/features/ide-redesign/components/editor-tour/editor-tour-got-questions'
|
||||
import { shouldIncludeElement } from '@/features/ide-react/util/rail-utils'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
|
||||
const moduleRailEntries = (
|
||||
importOverleafModules('railEntries') as {
|
||||
import: { default: RailElement }
|
||||
path: string
|
||||
}[]
|
||||
).map(({ import: { default: element } }) => element)
|
||||
|
||||
const moduleRailPopovers = (
|
||||
importOverleafModules('railPopovers') as {
|
||||
import: {
|
||||
default: {
|
||||
key: string
|
||||
Component: FC<{ ref: RefObject<HTMLAnchorElement> }>
|
||||
ref: RefObject<HTMLAnchorElement>
|
||||
hide: boolean | (() => boolean)
|
||||
}
|
||||
}
|
||||
path: string
|
||||
}[]
|
||||
).map(({ import: { default: element } }) => element)
|
||||
|
||||
export const RailLayout = () => {
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
const { t } = useTranslation()
|
||||
const { selectedTab, openTab, isOpen, setIsOpen, togglePane, selectTab } =
|
||||
useRailContext()
|
||||
const { features } = useProjectContext()
|
||||
const { isRestrictedTokenMember } = useEditorContext()
|
||||
const gitBridgeEnabled = getMeta('ol-gitBridgeEnabled')
|
||||
const { isOverleaf } = getMeta('ol-ExposedSettings')
|
||||
|
||||
const { view, setLeftMenuShown } = useLayoutContext()
|
||||
|
||||
const { markMessagesAsRead } = useChatContext()
|
||||
|
||||
const isHistoryView = view === 'history'
|
||||
|
||||
const fileTreeRef = useRef<HTMLAnchorElement>(null)
|
||||
const settingsRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useEventListener(
|
||||
'ui:select-rail-tab',
|
||||
useCallback(
|
||||
(event: Event) => {
|
||||
const {
|
||||
detail: { tab, open },
|
||||
} = event as CustomEvent<{
|
||||
tab: RailTabKey
|
||||
open: boolean
|
||||
}>
|
||||
selectTab(tab)
|
||||
setIsOpen(open)
|
||||
},
|
||||
[selectTab, setIsOpen]
|
||||
)
|
||||
)
|
||||
|
||||
const railTabs: RailElement[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'file-tree',
|
||||
icon: 'description',
|
||||
title: t('file_tree'),
|
||||
component: <FileTreeOutlinePanel />,
|
||||
// NOTE: We always need to mount the file tree on first load
|
||||
// since it is responsible for opening the initial document.
|
||||
mountOnFirstLoad: true,
|
||||
ref: fileTreeRef,
|
||||
},
|
||||
{
|
||||
key: 'full-project-search',
|
||||
icon: 'search',
|
||||
title: t('project_search'),
|
||||
component: <FullProjectSearchPanel />,
|
||||
hide: !hasFullProjectSearch,
|
||||
},
|
||||
{
|
||||
key: 'integrations',
|
||||
icon: 'integration_instructions',
|
||||
title: t('integrations'),
|
||||
component: <IntegrationsPanel />,
|
||||
hide: !isOverleaf && !gitBridgeEnabled,
|
||||
},
|
||||
{
|
||||
key: 'review-panel',
|
||||
icon: 'rate_review',
|
||||
title: t('review_panel'),
|
||||
component: null,
|
||||
hide: !features.trackChangesVisible,
|
||||
disabled: view !== 'editor',
|
||||
},
|
||||
{
|
||||
key: 'chat',
|
||||
icon: 'forum',
|
||||
component: <ChatPane />,
|
||||
indicator: <ChatIndicator />,
|
||||
title: t('chat'),
|
||||
hide:
|
||||
!getMeta('ol-capabilities')?.includes('chat') ||
|
||||
isRestrictedTokenMember,
|
||||
},
|
||||
...moduleRailEntries,
|
||||
],
|
||||
[
|
||||
t,
|
||||
features.trackChangesVisible,
|
||||
view,
|
||||
isRestrictedTokenMember,
|
||||
isOverleaf,
|
||||
gitBridgeEnabled,
|
||||
]
|
||||
)
|
||||
|
||||
const railActions: RailAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'support',
|
||||
icon: 'help',
|
||||
title: t('help'),
|
||||
dropdown: <RailHelpDropdown />,
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
icon: 'settings',
|
||||
title: t('settings'),
|
||||
action: () => {
|
||||
sendEvent('rail-click', { tab: 'settings' })
|
||||
setLeftMenuShown(true)
|
||||
},
|
||||
ref: settingsRef,
|
||||
},
|
||||
],
|
||||
[setLeftMenuShown, t, sendEvent]
|
||||
)
|
||||
|
||||
useCommandProvider(
|
||||
() => [
|
||||
{
|
||||
id: 'open-settings',
|
||||
handler: () => {
|
||||
setLeftMenuShown(true)
|
||||
},
|
||||
label: t('settings'),
|
||||
},
|
||||
],
|
||||
[t, setLeftMenuShown]
|
||||
)
|
||||
|
||||
const onTabSelect = useCallback(
|
||||
(key: string | null) => {
|
||||
if (key === selectedTab) {
|
||||
togglePane()
|
||||
sendEvent('rail-click', { tab: key, type: 'toggle' })
|
||||
} else {
|
||||
// HACK: Apparently the onSelect event is triggered with href attributes
|
||||
// from DropdownItems
|
||||
if (
|
||||
!railTabs.some(tab =>
|
||||
typeof tab.hide === 'function'
|
||||
? !tab.hide()
|
||||
: !tab.hide && tab.key === key
|
||||
)
|
||||
) {
|
||||
// Attempting to open a non-existent tab
|
||||
return
|
||||
}
|
||||
const keyOrDefault = (key ?? 'file-tree') as RailTabKey
|
||||
// Change the selected tab and make sure it's open
|
||||
openTab(keyOrDefault)
|
||||
sendEvent('rail-click', { tab: keyOrDefault })
|
||||
if (keyOrDefault === 'full-project-search') {
|
||||
sendSearchEvent('search-open', {
|
||||
searchType: 'full-project',
|
||||
method: 'button',
|
||||
location: 'rail',
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'chat') {
|
||||
markMessagesAsRead()
|
||||
}
|
||||
}
|
||||
},
|
||||
[openTab, togglePane, selectedTab, railTabs, sendEvent, markMessagesAsRead]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const validTabKeys = railTabs
|
||||
.filter(shouldIncludeElement)
|
||||
.map(tab => tab.key)
|
||||
if (!validTabKeys.includes(selectedTab) && isOpen) {
|
||||
// If the selected tab is no longer valid (e.g. due to permissions changes),
|
||||
// switch back to the file tree
|
||||
openTab('file-tree')
|
||||
}
|
||||
}, [railTabs, selectedTab, openTab, isOpen])
|
||||
|
||||
const isReviewPanelOpen =
|
||||
selectedTab === 'review-panel' && isOpen && !isHistoryView
|
||||
|
||||
const { tabsInRail, tabsInOverflow, tabWrapperRef } =
|
||||
useRailOverflow(railTabs)
|
||||
|
||||
const moreOptionsAction: RailAction = useMemo(() => {
|
||||
return {
|
||||
key: 'more-options',
|
||||
icon: 'more_vert',
|
||||
title: t('more_options'),
|
||||
hide: tabsInOverflow.length === 0,
|
||||
dropdown: (
|
||||
<RailOverflowDropdown
|
||||
tabs={tabsInOverflow}
|
||||
isOpen={isOpen}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
),
|
||||
}
|
||||
}, [t, isOpen, selectedTab, tabsInOverflow])
|
||||
|
||||
return (
|
||||
<TabContainer
|
||||
mountOnEnter // Only render when necessary (so that we can lazy load tab content)
|
||||
unmountOnExit={false} // TODO: Should we unmount the tabs when they're not used?
|
||||
transition={false}
|
||||
activeKey={selectedTab}
|
||||
onSelect={onTabSelect}
|
||||
id="ide-rail-tabs"
|
||||
>
|
||||
{/* The <Nav> element is a "div" and has a "role="tablist"".
|
||||
But it should be identified as a navigation landmark.
|
||||
Therefore, we nest them: the parent <nav> is the landmark, and its child gets the "role="tablist"". */}
|
||||
<nav
|
||||
className={classNames('ide-rail', { hidden: isHistoryView })}
|
||||
aria-label={t('sidebar')}
|
||||
>
|
||||
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
||||
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
|
||||
{tabsInRail
|
||||
.filter(shouldIncludeElement)
|
||||
.map(({ icon, key, indicator, title, disabled, ref, tab }) => {
|
||||
const Component = tab ?? RailTab
|
||||
return (
|
||||
<Component
|
||||
open={isOpen && selectedTab === key}
|
||||
key={key}
|
||||
eventKey={key}
|
||||
icon={icon}
|
||||
indicator={indicator}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<RailActionElement key="more-options" action={moreOptionsAction} />
|
||||
</div>
|
||||
<nav aria-label={t('help_editor_settings')}>
|
||||
{railActions.map(action => (
|
||||
<RailActionElement
|
||||
key={action.key}
|
||||
action={action}
|
||||
ref={action.ref}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</Nav>
|
||||
</nav>
|
||||
<EditorTourRailTooltip target={fileTreeRef.current} />
|
||||
<EditorTourThemeTooltip target={settingsRef.current} />
|
||||
<EditorTourGotQuestionsTooltip target={settingsRef.current} />
|
||||
{moduleRailPopovers
|
||||
.filter(shouldIncludeElement)
|
||||
.map(({ key, Component, ref }) => (
|
||||
<Component key={key} ref={ref} />
|
||||
))}
|
||||
<RailPanel
|
||||
isReviewPanelOpen={isReviewPanelOpen}
|
||||
isHistoryView={isHistoryView}
|
||||
railTabs={railTabs}
|
||||
/>
|
||||
<RailResizeHandle isReviewPanelOpen={isReviewPanelOpen} />
|
||||
<RailModals />
|
||||
</TabContainer>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user