rail-context.tsx 4.4 KB

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