import { FC, forwardRef, ReactElement, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap' import MaterialIcon, { AvailableUnfilledIcon, } from '@/shared/components/material-icon' import { Panel } from 'react-resizable-panels' import { useLayoutContext } from '@/shared/context/layout-context' import ErrorIndicator from './error-logs/error-indicator' import { RailModalKey, RailTabKey, useRailContext, } from '../contexts/rail-context' import FileTreeOutlinePanel from './file-tree/file-tree-outline-panel' import { ChatIndicator, ChatPane } from './chat/chat' import getMeta from '@/utils/meta' 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 IntegrationsPanel from './integrations-panel/integrations-panel' import { Dropdown, DropdownDivider, DropdownItem, DropdownMenu, DropdownToggle, } from '@/features/ui/components/bootstrap-5/dropdown-menu' import { RailHelpShowHotkeysModal } from './help/keyboard-shortcuts' import { RailHelpContactUsModal } from './help/contact-us' import { HistorySidebar } from '@/features/ide-react/components/history-sidebar' import DictionarySettingsModal from './settings/editor-settings/dictionary-settings-modal' import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import { useChatContext } from '@/features/chat/context/chat-context' import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics' import { FullProjectSearchPanel, hasFullProjectSearch, } from './full-project-search-panel' import { sendSearchEvent } from '@/features/event-tracking/search-events' import ErrorLogsPanel from './error-logs/error-logs-panel' import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' import OldErrorPane from './error-logs/old-error-pane' import { useFeatureFlag } from '@/shared/context/split-test-context' import { useSurveyUrl } from '../hooks/use-survey-url' import { useProjectContext } from '@/shared/context/project-context' import usePreviousValue from '@/shared/hooks/use-previous-value' import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider' type RailElement = { icon: AvailableUnfilledIcon key: RailTabKey component: ReactElement | null indicator?: ReactElement title: string hide?: boolean disabled?: boolean mountOnFirstLoad?: boolean } type RailActionButton = { key: string icon: AvailableUnfilledIcon title: string action: () => void } type RailDropdown = { key: string icon: AvailableUnfilledIcon title: string dropdown: ReactElement } type RailAction = RailDropdown | RailActionButton const RAIL_MODALS: { key: RailModalKey modalComponentFunction: FC<{ show: boolean }> }[] = [ { key: 'keyboard-shortcuts', modalComponentFunction: RailHelpShowHotkeysModal, }, { key: 'contact-us', modalComponentFunction: RailHelpContactUsModal, }, { key: 'dictionary', modalComponentFunction: DictionarySettingsModal, }, ] export const RailLayout = () => { const { sendEvent } = useEditorAnalytics() const { t } = useTranslation() const { activeModal, selectedTab, openTab, isOpen, setIsOpen, panelRef, handlePaneCollapse, handlePaneExpand, togglePane, setResizing, } = useRailContext() const { logEntries } = useCompileContext() const { features } = useProjectContext() const errorLogsDisabled = !logEntries const { view, setLeftMenuShown } = useLayoutContext() const { markMessagesAsRead } = useChatContext() const isHistoryView = view === 'history' const newErrorlogs = useFeatureFlag('new-editor-error-logs-redesign') 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: , }, { 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'), }, { key: 'errors', icon: 'report', title: t('error_log'), component: newErrorlogs ? : , indicator: , disabled: errorLogsDisabled, }, ], [t, features.trackChangesVisible, newErrorlogs, errorLogsDisabled, view] ) 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 => !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] ) const isReviewPanelOpen = selectedTab === 'review-panel' const prevTab = usePreviousValue(selectedTab) const tabHasChanged = useMemo(() => { return prevTab !== selectedTab }, [prevTab, selectedTab]) const onCollapse = useCallback(() => { if (!tabHasChanged) { handlePaneCollapse() } }, [tabHasChanged, handlePaneCollapse]) return ( {railTabs .filter(({ hide }) => !hide) .map(({ icon, key, indicator, title, disabled }) => ( ))} {railActions?.map(action => ( ))} {isHistoryView && } {railTabs .filter(({ hide }) => !hide) .map(({ key, component, mountOnFirstLoad }) => ( {component} ))} {RAIL_MODALS.map(({ key, modalComponentFunction: Component }) => ( ))} ) } const RailTab = forwardRef< HTMLAnchorElement, { icon: AvailableUnfilledIcon eventKey: string open: boolean indicator?: ReactElement title: string disabled?: boolean } >(({ icon, eventKey, open, indicator, title, disabled = false }, ref) => { return ( {open ? ( ) : ( )} {indicator} ) }) RailTab.displayName = 'RailTab' const RailActionElement = ({ action }: { action: RailAction }) => { const onActionClick = useCallback(() => { if ('action' in action) { action.action() } }, [action]) if ('dropdown' in action) { return ( {action.dropdown} ) } else { return ( ) } } const 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]) const surveyURL = useSurveyUrl() return ( {t('keyboard_shortcuts')} {t('documentation')} {showSupport && ( {t('contact_us')} )} {t('give_feedback')} ) }