diff --git a/services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx b/services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx index 2efad2a561..6bc8629982 100644 --- a/services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx @@ -9,8 +9,8 @@ import { useUserContext } from '@/shared/context/user-context' import { lazy, Suspense, useEffect } from 'react' import { useTranslation } from 'react-i18next' import classNames from 'classnames' -import { RailIndicator } from '../rail-indicator' -import RailPanelHeader from '../rail-panel-header' +import { RailIndicator } from '../rail/rail-indicator' +import RailPanelHeader from '../rail/rail-panel-header' const MessageList = lazy(() => import('../../../chat/components/message-list')) diff --git a/services/web/frontend/js/features/ide-redesign/components/error-logs/error-indicator.tsx b/services/web/frontend/js/features/ide-redesign/components/error-logs/error-indicator.tsx index 7b721a1d51..f534176c43 100644 --- a/services/web/frontend/js/features/ide-redesign/components/error-logs/error-indicator.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/error-logs/error-indicator.tsx @@ -1,5 +1,5 @@ import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' -import { RailIndicator } from '../rail-indicator' +import { RailIndicator } from '../rail/rail-indicator' export default function ErrorIndicator() { const { logEntries } = useCompileContext() diff --git a/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-header.tsx b/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-header.tsx index 2f3a54b095..b2038ff58d 100644 --- a/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-header.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-header.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import RailPanelHeader from '../rail-panel-header' +import RailPanelHeader from '../rail/rail-panel-header' import OLIconButton from '@/features/ui/components/ol/ol-icon-button' import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' import { diff --git a/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx b/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx index e477602e3e..e0794eea45 100644 --- a/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx @@ -1,7 +1,7 @@ import { ElementType } from 'react' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' import { useTranslation } from 'react-i18next' -import RailPanelHeader from '../rail-panel-header' +import RailPanelHeader from '../rail/rail-panel-header' const integrationPanelComponents = importOverleafModules( 'integrationPanelComponents' diff --git a/services/web/frontend/js/features/ide-redesign/components/main-layout.tsx b/services/web/frontend/js/features/ide-redesign/components/main-layout.tsx index 46b147f384..effff8f2c8 100644 --- a/services/web/frontend/js/features/ide-redesign/components/main-layout.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/main-layout.tsx @@ -2,7 +2,7 @@ import { Panel, PanelGroup } from 'react-resizable-panels' import classNames from 'classnames' import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle' import PdfPreview from '@/features/pdf-preview/components/pdf-preview' -import { RailLayout } from './rail' +import { RailLayout } from './rail/rail' import { Toolbar } from './toolbar/toolbar' import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler' import { useTranslation } from 'react-i18next' diff --git a/services/web/frontend/js/features/ide-redesign/components/rail.tsx b/services/web/frontend/js/features/ide-redesign/components/rail.tsx deleted file mode 100644 index 3643c03841..0000000000 --- a/services/web/frontend/js/features/ide-redesign/components/rail.tsx +++ /dev/null @@ -1,494 +0,0 @@ -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 ( - - {/* The element is a "div" and has a "role="tablist"". - But it should be identified as a navigation landmark. - Therefore, we nest them: the parent is the landmark, and its child gets the "role="tablist"". */} - - - {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')} - - - ) -} diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail-action-element.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-action-element.tsx new file mode 100644 index 0000000000..5faf1a4257 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail-action-element.tsx @@ -0,0 +1,81 @@ +import MaterialIcon, { + AvailableUnfilledIcon, +} from '@/shared/components/material-icon' +import { ReactElement, useCallback } from 'react' +import { + Dropdown, + DropdownToggle, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' + +type RailActionButton = { + key: string + icon: AvailableUnfilledIcon + title: string + action: () => void +} + +type RailDropdown = { + key: string + icon: AvailableUnfilledIcon + title: string + dropdown: ReactElement +} + +export type RailAction = RailDropdown | RailActionButton + +export default function RailActionElement({ action }: { action: RailAction }) { + const onActionClick = useCallback(() => { + if ('action' in action) { + action.action() + } + }, [action]) + + if ('dropdown' in action) { + return ( + + + + + + + + + {action.dropdown} + + ) + } else { + return ( + + + + + + ) + } +} diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail-help-dropdown.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-help-dropdown.tsx new file mode 100644 index 0000000000..d62b063ca7 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail-help-dropdown.tsx @@ -0,0 +1,53 @@ +import getMeta from '@/utils/meta' +import { useTranslation } from 'react-i18next' +import { useRailContext } from '../../contexts/rail-context' +import { useCallback } from 'react' +import { useSurveyUrl } from '../../hooks/use-survey-url' +import { + DropdownDivider, + DropdownItem, + DropdownMenu, +} from '@/features/ui/components/bootstrap-5/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]) + const surveyURL = useSurveyUrl() + + return ( + + + {t('keyboard_shortcuts')} + + + {t('documentation')} + + + {showSupport && ( + + {t('contact_us')} + + )} + + {t('give_feedback')} + + + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/rail-indicator.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-indicator.tsx similarity index 100% rename from services/web/frontend/js/features/ide-redesign/components/rail-indicator.tsx rename to services/web/frontend/js/features/ide-redesign/components/rail/rail-indicator.tsx diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail-modals.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-modals.tsx new file mode 100644 index 0000000000..0cbf0d8f23 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail-modals.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react' +import { RailModalKey, useRailContext } from '../../contexts/rail-context' +import { RailHelpContactUsModal } from '../help/contact-us' +import { RailHelpShowHotkeysModal } from '../help/keyboard-shortcuts' +import DictionarySettingsModal from '../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 }) => ( + + )) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-panel-header.tsx similarity index 94% rename from services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx rename to services/web/frontend/js/features/ide-redesign/components/rail/rail-panel-header.tsx index 569c9c99bc..8d46c548d7 100644 --- a/services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail-panel-header.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import { useRailContext } from '../contexts/rail-context' +import { useRailContext } from '../../contexts/rail-context' import OLIconButton from '@/features/ui/components/ol/ol-icon-button' import React from 'react' import OLTooltip from '@/features/ui/components/ol/ol-tooltip' diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail-panel.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-panel.tsx new file mode 100644 index 0000000000..7585bc25ed --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail-panel.tsx @@ -0,0 +1,76 @@ +import { useFeatureFlag } from '@/shared/context/split-test-context' +import { Panel } from 'react-resizable-panels' +import { useRailContext } from '../../contexts/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 '../../utils/rail-types' + +export default function RailPanel({ + isReviewPanelOpen, + isHistoryView, + railTabs, +}: { + isReviewPanelOpen: boolean + isHistoryView: boolean + railTabs: RailElement[] +}) { + const { selectedTab, panelRef, handlePaneExpand, handlePaneCollapse } = + useRailContext() + + const newErrorlogs = useFeatureFlag('new-editor-error-logs-redesign') + + const prevTab = usePreviousValue(selectedTab) + + const tabHasChanged = useMemo(() => { + return prevTab !== selectedTab + }, [prevTab, selectedTab]) + + const onCollapse = useCallback(() => { + if (!tabHasChanged) { + handlePaneCollapse() + } + }, [tabHasChanged, handlePaneCollapse]) + + return ( + + {isHistoryView && } + + + {railTabs + .filter(({ hide }) => !hide) + .map(({ key, component, mountOnFirstLoad }) => ( + + {component} + + ))} + + + + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail-resize-handle.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-resize-handle.tsx new file mode 100644 index 0000000000..d1a3dc4fad --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail-resize-handle.tsx @@ -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 '../../contexts/rail-context' +import { useTranslation } from 'react-i18next' + +export default function RailResizeHandle({ + isReviewPanelOpen, +}: { + isReviewPanelOpen: boolean +}) { + const { isOpen, setIsOpen, togglePane, setResizing } = useRailContext() + const { t } = useTranslation() + + return ( + + + + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail-tab.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail-tab.tsx new file mode 100644 index 0000000000..4b2ea378e3 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail-tab.tsx @@ -0,0 +1,56 @@ +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import MaterialIcon, { + AvailableUnfilledIcon, +} from '@/shared/components/material-icon' +import classNames from 'classnames' +import { forwardRef, ReactElement } from 'react' +import { NavLink } from 'react-bootstrap' + +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' + +export default RailTab diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail.tsx new file mode 100644 index 0000000000..4cf0c4bd14 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail.tsx @@ -0,0 +1,216 @@ +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { Nav, TabContainer } from 'react-bootstrap' +import { useLayoutContext } from '@/shared/context/layout-context' +import ErrorIndicator from '../error-logs/error-indicator' +import { 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 classNames from 'classnames' +import IntegrationsPanel from '../integrations-panel/integrations-panel' +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 { 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 '../../utils/rail-types' +import RailPanel from './rail-panel' +import RailResizeHandle from './rail-resize-handle' +import RailModals from './rail-modals' + +export const RailLayout = () => { + const { sendEvent } = useEditorAnalytics() + const { t } = useTranslation() + const { selectedTab, openTab, isOpen, togglePane } = 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' + + return ( + + {/* The element is a "div" and has a "role="tablist"". + But it should be identified as a navigation landmark. + Therefore, we nest them: the parent is the landmark, and its child gets the "role="tablist"". */} + + + {railTabs + .filter(({ hide }) => !hide) + .map(({ icon, key, indicator, title, disabled }) => ( + + ))} + + + {railActions?.map(action => ( + + ))} + + + + + + + + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/utils/rail-types.ts b/services/web/frontend/js/features/ide-redesign/utils/rail-types.ts new file mode 100644 index 0000000000..244b8b63ad --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/utils/rail-types.ts @@ -0,0 +1,14 @@ +import { AvailableUnfilledIcon } from '@/shared/components/material-icon' +import { RailTabKey } from '../contexts/rail-context' +import { ReactElement } from 'react' + +export type RailElement = { + icon: AvailableUnfilledIcon + key: RailTabKey + component: ReactElement | null + indicator?: ReactElement + title: string + hide?: boolean + disabled?: boolean + mountOnFirstLoad?: boolean +} diff --git a/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx b/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx index 69acc9e38c..8a529ffff5 100644 --- a/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx +++ b/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx @@ -36,7 +36,7 @@ import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree- import { FullProjectSearchResults } from './full-project-search-results' import { signalWithTimeout } from '@/utils/abort-signal' import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils' -import RailPanelHeader from '@/features/ide-redesign/components/rail-panel-header' +import RailPanelHeader from '@/features/ide-redesign/components/rail/rail-panel-header' import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme' const FullProjectSearchUI: FC = () => {