rail-context.tsx 4.0 KB

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