rail.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import { FC, ReactElement, useCallback, useMemo } from 'react'
  2. import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap-5'
  3. import MaterialIcon, {
  4. AvailableUnfilledIcon,
  5. } from '@/shared/components/material-icon'
  6. import { Panel } from 'react-resizable-panels'
  7. import { useLayoutContext } from '@/shared/context/layout-context'
  8. import { ErrorIndicator, ErrorPane } from './errors'
  9. import {
  10. RailModalKey,
  11. RailTabKey,
  12. useRailContext,
  13. } from '../contexts/rail-context'
  14. import FileTreeOutlinePanel from './file-tree-outline-panel'
  15. import { ChatIndicator, ChatPane } from './chat/chat'
  16. import getMeta from '@/utils/meta'
  17. import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
  18. import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
  19. import { useTranslation } from 'react-i18next'
  20. import classNames from 'classnames'
  21. import IntegrationsPanel from './integrations-panel/integrations-panel'
  22. import OLButton from '@/features/ui/components/ol/ol-button'
  23. import {
  24. Dropdown,
  25. DropdownDivider,
  26. DropdownItem,
  27. DropdownMenu,
  28. DropdownToggle,
  29. } from '@/features/ui/components/bootstrap-5/dropdown-menu'
  30. import { RailHelpShowHotkeysModal } from './help/keyboard-shortcuts'
  31. import { RailHelpContactUsModal } from './help/contact-us'
  32. import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
  33. import DictionarySettingsModal from './settings/editor-settings/dictionary-settings-modal'
  34. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  35. type RailElement = {
  36. icon: AvailableUnfilledIcon
  37. key: RailTabKey
  38. component: ReactElement | null
  39. indicator?: ReactElement
  40. title: string
  41. hide?: boolean
  42. }
  43. type RailActionButton = {
  44. key: string
  45. icon: AvailableUnfilledIcon
  46. title: string
  47. action: () => void
  48. }
  49. type RailDropdown = {
  50. key: string
  51. icon: AvailableUnfilledIcon
  52. title: string
  53. dropdown: ReactElement
  54. }
  55. type RailAction = RailDropdown | RailActionButton
  56. const RAIL_MODALS: {
  57. key: RailModalKey
  58. modalComponentFunction: FC<{ show: boolean }>
  59. }[] = [
  60. {
  61. key: 'keyboard-shortcuts',
  62. modalComponentFunction: RailHelpShowHotkeysModal,
  63. },
  64. {
  65. key: 'contact-us',
  66. modalComponentFunction: RailHelpContactUsModal,
  67. },
  68. {
  69. key: 'dictionary',
  70. modalComponentFunction: DictionarySettingsModal,
  71. },
  72. ]
  73. export const RailLayout = () => {
  74. const { t } = useTranslation()
  75. const {
  76. activeModal,
  77. selectedTab,
  78. openTab,
  79. isOpen,
  80. setIsOpen,
  81. panelRef,
  82. handlePaneCollapse,
  83. handlePaneExpand,
  84. togglePane,
  85. setResizing,
  86. } = useRailContext()
  87. const { view, setLeftMenuShown } = useLayoutContext()
  88. const isHistoryView = view === 'history'
  89. const railTabs: RailElement[] = useMemo(
  90. () => [
  91. {
  92. key: 'file-tree',
  93. icon: 'description',
  94. title: t('file_tree'),
  95. component: <FileTreeOutlinePanel />,
  96. },
  97. {
  98. key: 'integrations',
  99. icon: 'integration_instructions',
  100. title: t('integrations'),
  101. component: <IntegrationsPanel />,
  102. },
  103. {
  104. key: 'review-panel',
  105. icon: 'rate_review',
  106. title: t('review_panel'),
  107. component: null,
  108. },
  109. {
  110. key: 'chat',
  111. icon: 'forum',
  112. component: <ChatPane />,
  113. indicator: <ChatIndicator />,
  114. title: t('chat'),
  115. hide: !getMeta('ol-chatEnabled'),
  116. },
  117. {
  118. key: 'errors',
  119. icon: 'report',
  120. title: t('error_log'),
  121. component: <ErrorPane />,
  122. indicator: <ErrorIndicator />,
  123. },
  124. ],
  125. [t]
  126. )
  127. const railActions: RailAction[] = useMemo(
  128. () => [
  129. {
  130. key: 'support',
  131. icon: 'help',
  132. title: t('help'),
  133. dropdown: <RailHelpDropdown />,
  134. },
  135. {
  136. key: 'settings',
  137. icon: 'settings',
  138. title: t('settings'),
  139. action: () => setLeftMenuShown(true),
  140. },
  141. ],
  142. [setLeftMenuShown, t]
  143. )
  144. const onTabSelect = useCallback(
  145. (key: string | null) => {
  146. if (key === selectedTab) {
  147. togglePane()
  148. } else {
  149. // HACK: Apparently the onSelect event is triggered with href attributes
  150. // from DropdownItems
  151. if (!railTabs.some(tab => !tab.hide && tab.key === key)) {
  152. // Attempting to open a non-existent tab
  153. return
  154. }
  155. // Change the selected tab and make sure it's open
  156. openTab((key ?? 'file-tree') as RailTabKey)
  157. }
  158. },
  159. [openTab, togglePane, selectedTab, railTabs]
  160. )
  161. const isReviewPanelOpen = selectedTab === 'review-panel'
  162. return (
  163. <TabContainer
  164. mountOnEnter // Only render when necessary (so that we can lazy load tab content)
  165. unmountOnExit={false} // TODO: Should we unmount the tabs when they're not used?
  166. transition={false}
  167. activeKey={selectedTab}
  168. onSelect={onTabSelect}
  169. id="ide-rail-tabs"
  170. >
  171. <div className={classNames('ide-rail', { hidden: isHistoryView })}>
  172. <Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
  173. {railTabs
  174. .filter(({ hide }) => !hide)
  175. .map(({ icon, key, indicator, title }) => (
  176. <RailTab
  177. open={isOpen && selectedTab === key}
  178. key={key}
  179. eventKey={key}
  180. icon={icon}
  181. indicator={indicator}
  182. title={title}
  183. />
  184. ))}
  185. <div className="flex-grow-1" />
  186. {railActions?.map(action => (
  187. <RailActionElement key={action.key} action={action} />
  188. ))}
  189. </Nav>
  190. </div>
  191. <Panel
  192. id="ide-redesign-sidebar-panel"
  193. className={classNames({ hidden: isReviewPanelOpen })}
  194. order={1}
  195. defaultSize={15}
  196. minSize={5}
  197. maxSize={80}
  198. ref={panelRef}
  199. collapsible
  200. onCollapse={handlePaneCollapse}
  201. onExpand={handlePaneExpand}
  202. >
  203. {isHistoryView && <HistorySidebar />}
  204. <div
  205. className={classNames('ide-rail-content', {
  206. hidden: isHistoryView,
  207. })}
  208. >
  209. <Tab.Content>
  210. {railTabs
  211. .filter(({ hide }) => !hide)
  212. .map(({ key, component }) => (
  213. <Tab.Pane eventKey={key} key={key}>
  214. {component}
  215. </Tab.Pane>
  216. ))}
  217. </Tab.Content>
  218. </div>
  219. </Panel>
  220. <HorizontalResizeHandle
  221. className={classNames({ hidden: isReviewPanelOpen })}
  222. resizable
  223. hitAreaMargins={{ coarse: 0, fine: 0 }}
  224. onDoubleClick={togglePane}
  225. onDragging={setResizing}
  226. >
  227. <HorizontalToggler
  228. id="ide-redesign-sidebar-panel"
  229. togglerType="west"
  230. isOpen={isOpen}
  231. setIsOpen={setIsOpen}
  232. tooltipWhenOpen={t('tooltip_hide_panel')}
  233. tooltipWhenClosed={t('tooltip_show_panel')}
  234. />
  235. </HorizontalResizeHandle>
  236. {RAIL_MODALS.map(({ key, modalComponentFunction: Component }) => (
  237. <Component key={key} show={activeModal === key} />
  238. ))}
  239. </TabContainer>
  240. )
  241. }
  242. const RailTab = ({
  243. icon,
  244. eventKey,
  245. open,
  246. indicator,
  247. title,
  248. }: {
  249. icon: AvailableUnfilledIcon
  250. eventKey: string
  251. open: boolean
  252. indicator?: ReactElement
  253. title: string
  254. }) => {
  255. return (
  256. <OLTooltip
  257. id={`rail-tab-tooltip-${eventKey}`}
  258. description={title}
  259. overlayProps={{ delay: 0, placement: 'right' }}
  260. >
  261. <NavLink
  262. eventKey={eventKey}
  263. className={classNames('ide-rail-tab-link', {
  264. 'open-rail': open,
  265. })}
  266. >
  267. {open ? (
  268. <MaterialIcon className="ide-rail-tab-link-icon" type={icon} />
  269. ) : (
  270. <MaterialIcon
  271. className="ide-rail-tab-link-icon"
  272. type={icon}
  273. unfilled
  274. />
  275. )}
  276. {indicator}
  277. </NavLink>
  278. </OLTooltip>
  279. )
  280. }
  281. const RailActionElement = ({ action }: { action: RailAction }) => {
  282. const icon = (
  283. <MaterialIcon
  284. className="ide-rail-tab-link-icon"
  285. type={action.icon}
  286. unfilled
  287. />
  288. )
  289. const onActionClick = useCallback(() => {
  290. if ('action' in action) {
  291. action.action()
  292. }
  293. }, [action])
  294. if ('dropdown' in action) {
  295. return (
  296. <Dropdown align="end" drop="end">
  297. <OLTooltip
  298. id={`rail-dropdown-tooltip-${action.key}`}
  299. description={action.title}
  300. overlayProps={{ delay: 0, placement: 'right' }}
  301. >
  302. <span>
  303. <DropdownToggle
  304. id="rail-help-dropdown-btn"
  305. className="ide-rail-tab-link ide-rail-tab-button ide-rail-tab-dropdown"
  306. as="button"
  307. >
  308. {icon}
  309. </DropdownToggle>
  310. </span>
  311. </OLTooltip>
  312. {action.dropdown}
  313. </Dropdown>
  314. )
  315. } else {
  316. return (
  317. <OLTooltip
  318. id={`rail-tab-tooltip-${action.key}`}
  319. description={action.title}
  320. overlayProps={{ delay: 0, placement: 'right' }}
  321. >
  322. <button
  323. onClick={onActionClick}
  324. className="ide-rail-tab-link ide-rail-tab-button"
  325. type="button"
  326. >
  327. {icon}
  328. </button>
  329. </OLTooltip>
  330. )
  331. }
  332. }
  333. export const RailPanelHeader: FC<{ title: string }> = ({ title }) => {
  334. const { handlePaneCollapse } = useRailContext()
  335. return (
  336. <header className="rail-panel-header">
  337. <h4 className="rail-panel-title">{title}</h4>
  338. <OLButton
  339. onClick={handlePaneCollapse}
  340. className="rail-panel-header-button-subdued"
  341. size="sm"
  342. >
  343. <MaterialIcon type="close" />
  344. </OLButton>
  345. </header>
  346. )
  347. }
  348. const RailHelpDropdown = () => {
  349. const showSupport = getMeta('ol-showSupport')
  350. const { t } = useTranslation()
  351. const { setActiveModal } = useRailContext()
  352. const openKeyboardShortcutsModal = useCallback(() => {
  353. setActiveModal('keyboard-shortcuts')
  354. }, [setActiveModal])
  355. const openContactUsModal = useCallback(() => {
  356. setActiveModal('contact-us')
  357. }, [setActiveModal])
  358. return (
  359. <DropdownMenu>
  360. <DropdownItem onClick={openKeyboardShortcutsModal}>
  361. {t('keyboard_shortcuts')}
  362. </DropdownItem>
  363. <DropdownItem
  364. href="/learn"
  365. role="menuitem"
  366. target="_blank"
  367. rel="noopener noreferrer"
  368. >
  369. {t('documentation')}
  370. </DropdownItem>
  371. <DropdownDivider />
  372. {showSupport && (
  373. <DropdownItem onClick={openContactUsModal}>
  374. {t('contact_us')}
  375. </DropdownItem>
  376. )}
  377. <DropdownItem
  378. href="https://forms.gle/soyVStc5qDx9na1Z6"
  379. role="menuitem"
  380. target="_blank"
  381. rel="noopener noreferrer"
  382. >
  383. {t('give_feedback')}
  384. </DropdownItem>
  385. </DropdownMenu>
  386. )
  387. }