import { FC, RefObject, useCallback, useEffect, useMemo } 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/file-tree/components/file-tree-outline-panel' import ChatPane from '@/features/chat/components/chat-pane' import ChatIndicator from '@/features/chat/components/chat-indicator' import getMeta from '@/utils/meta' import classNames from 'classnames' import IntegrationsPanel from '@/features/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-react/components/rail/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-react/hooks/use-rail-overflow' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' 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 }> ref: RefObject 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' 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: , // NOTE: We always need to mount the file tree on first load // since it is responsible for opening the initial document. mountOnFirstLoad: true, }, { key: 'full-project-search', icon: 'search', title: t('project_search'), component: , hide: !hasFullProjectSearch, }, { key: 'integrations', icon: 'integration_instructions', title: t('integrations'), component: , 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: , indicator: , 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: , }, { key: 'settings', icon: 'settings', title: t('settings'), action: () => { sendEvent('rail-click', { tab: 'settings' }) setLeftMenuShown(true) }, }, ], [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: ( ), } }, [t, isOpen, selectedTab, tabsInOverflow]) return ( {/* The