rail-context.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { sendSearchEvent } from '@/features/event-tracking/search-events'
  2. import useEventListener from '@/shared/hooks/use-event-listener'
  3. import { isMac } from '@/shared/utils/os'
  4. import {
  5. createContext,
  6. Dispatch,
  7. FC,
  8. SetStateAction,
  9. useCallback,
  10. useContext,
  11. useLayoutEffect,
  12. useMemo,
  13. useRef,
  14. useState,
  15. } from 'react'
  16. import { ImperativePanelHandle } from 'react-resizable-panels'
  17. export type RailTabKey =
  18. | 'file-tree'
  19. | 'integrations'
  20. | 'review-panel'
  21. | 'chat'
  22. | 'errors'
  23. | 'full-project-search'
  24. export type RailModalKey = 'keyboard-shortcuts' | 'contact-us' | 'dictionary'
  25. const RailContext = createContext<
  26. | {
  27. selectedTab: RailTabKey
  28. isOpen: boolean
  29. setIsOpen: Dispatch<SetStateAction<boolean>>
  30. panelRef: React.RefObject<ImperativePanelHandle>
  31. togglePane: () => void
  32. handlePaneExpand: () => void
  33. handlePaneCollapse: () => void
  34. resizing: boolean
  35. setResizing: Dispatch<SetStateAction<boolean>>
  36. activeModal: RailModalKey | null
  37. setActiveModal: Dispatch<SetStateAction<RailModalKey | null>>
  38. openTab: (tab: RailTabKey) => void
  39. }
  40. | undefined
  41. >(undefined)
  42. export const RailProvider: FC<React.PropsWithChildren> = ({ children }) => {
  43. const [isOpen, setIsOpen] = useState(true)
  44. const [resizing, setResizing] = useState(false)
  45. const [activeModal, setActiveModalInternal] = useState<RailModalKey | null>(
  46. null
  47. )
  48. const setActiveModal: Dispatch<SetStateAction<RailModalKey | null>> =
  49. useCallback(modalKey => {
  50. setActiveModalInternal(modalKey)
  51. }, [])
  52. const panelRef = useRef<ImperativePanelHandle>(null)
  53. const togglePane = useCallback(() => {
  54. setIsOpen(value => !value)
  55. }, [])
  56. const handlePaneExpand = useCallback(() => {
  57. setIsOpen(true)
  58. }, [])
  59. const handlePaneCollapse = useCallback(() => {
  60. setIsOpen(false)
  61. }, [])
  62. // NOTE: The file tree **MUST** be the first tab to be opened
  63. // since it is responsible for opening the initial document.
  64. const [selectedTab, setSelectedTab] = useState<RailTabKey>('file-tree')
  65. // Keep the panel collapse/expanded state in sync with isOpen and selectedTab
  66. useLayoutEffect(() => {
  67. const panelHandle = panelRef.current
  68. if (panelHandle) {
  69. if (isOpen) {
  70. panelHandle.expand()
  71. } else {
  72. panelHandle.collapse()
  73. }
  74. }
  75. }, [isOpen, selectedTab])
  76. const openTab = useCallback(
  77. (tab: RailTabKey) => {
  78. setSelectedTab(tab)
  79. setIsOpen(true)
  80. },
  81. [setIsOpen, setSelectedTab]
  82. )
  83. useEventListener(
  84. 'ui.toggle-review-panel',
  85. useCallback(() => {
  86. if (isOpen && selectedTab === 'review-panel') {
  87. handlePaneCollapse()
  88. } else {
  89. openTab('review-panel')
  90. }
  91. }, [handlePaneCollapse, selectedTab, isOpen, openTab])
  92. )
  93. useEventListener(
  94. 'keydown',
  95. useCallback(
  96. (event: KeyboardEvent) => {
  97. if (
  98. (isMac ? event.metaKey : event.ctrlKey) &&
  99. event.shiftKey &&
  100. event.code === 'KeyF'
  101. ) {
  102. event.preventDefault()
  103. sendSearchEvent('search-open', {
  104. searchType: 'full-project',
  105. method: 'keyboard',
  106. })
  107. openTab('full-project-search')
  108. }
  109. },
  110. [openTab]
  111. )
  112. )
  113. const value = useMemo(
  114. () => ({
  115. selectedTab,
  116. isOpen,
  117. setIsOpen,
  118. panelRef,
  119. togglePane,
  120. handlePaneExpand,
  121. handlePaneCollapse,
  122. resizing,
  123. setResizing,
  124. activeModal,
  125. setActiveModal,
  126. openTab,
  127. }),
  128. [
  129. selectedTab,
  130. isOpen,
  131. setIsOpen,
  132. panelRef,
  133. togglePane,
  134. handlePaneExpand,
  135. handlePaneCollapse,
  136. resizing,
  137. setResizing,
  138. activeModal,
  139. setActiveModal,
  140. openTab,
  141. ]
  142. )
  143. return <RailContext.Provider value={value}>{children}</RailContext.Provider>
  144. }
  145. export const useRailContext = () => {
  146. const context = useContext(RailContext)
  147. if (!context) {
  148. throw new Error('useRailContext is only available inside RailProvider')
  149. }
  150. return context
  151. }