rail-context.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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. }
  42. | undefined
  43. >(undefined)
  44. export const RailProvider: FC<React.PropsWithChildren> = ({ children }) => {
  45. const { projectId } = useProjectContext()
  46. const [isOpen, setIsOpen] = usePersistedState(
  47. `rail-is-open-${projectId}`,
  48. true
  49. )
  50. const [resizing, setResizing] = useState(false)
  51. const [activeModal, setActiveModalInternal] = useState<RailModalKey | null>(
  52. null
  53. )
  54. const setActiveModal: Dispatch<SetStateAction<RailModalKey | null>> =
  55. useCallback(modalKey => {
  56. setActiveModalInternal(modalKey)
  57. }, [])
  58. const panelRef = useRef<ImperativePanelHandle>(null)
  59. const togglePane = useCallback(() => {
  60. setIsOpen(value => !value)
  61. }, [setIsOpen])
  62. const handlePaneExpand = useCallback(() => {
  63. setIsOpen(true)
  64. }, [setIsOpen])
  65. const handlePaneCollapse = useCallback(() => {
  66. setIsOpen(false)
  67. }, [setIsOpen])
  68. const [selectedTab, setSelectedTab] = usePersistedState<RailTabKey>(
  69. `selected-rail-tab-${projectId}`,
  70. 'file-tree'
  71. )
  72. // Keep the panel collapse/expanded state in sync with isOpen and selectedTab
  73. useLayoutEffect(() => {
  74. const panelHandle = panelRef.current
  75. if (panelHandle) {
  76. if (isOpen) {
  77. panelHandle.expand()
  78. } else {
  79. panelHandle.collapse()
  80. }
  81. }
  82. }, [isOpen, selectedTab])
  83. const openTab = useCallback(
  84. (tab: RailTabKey) => {
  85. setSelectedTab(tab)
  86. setIsOpen(true)
  87. },
  88. [setIsOpen, setSelectedTab]
  89. )
  90. useEventListener(
  91. 'ui.toggle-review-panel',
  92. useCallback(() => {
  93. if (isOpen && selectedTab === 'review-panel') {
  94. handlePaneCollapse()
  95. } else {
  96. openTab('review-panel')
  97. }
  98. }, [handlePaneCollapse, selectedTab, isOpen, openTab])
  99. )
  100. useEventListener(
  101. 'keydown',
  102. useCallback(
  103. (event: KeyboardEvent) => {
  104. if (
  105. (isMac ? event.metaKey : event.ctrlKey) &&
  106. event.shiftKey &&
  107. event.code === 'KeyF'
  108. ) {
  109. event.preventDefault()
  110. sendSearchEvent('search-open', {
  111. searchType: 'full-project',
  112. method: 'keyboard',
  113. })
  114. openTab('full-project-search')
  115. }
  116. },
  117. [openTab]
  118. )
  119. )
  120. const value = useMemo(
  121. () => ({
  122. selectedTab,
  123. isOpen,
  124. setIsOpen,
  125. panelRef,
  126. togglePane,
  127. handlePaneExpand,
  128. handlePaneCollapse,
  129. resizing,
  130. setResizing,
  131. activeModal,
  132. setActiveModal,
  133. openTab,
  134. }),
  135. [
  136. selectedTab,
  137. isOpen,
  138. setIsOpen,
  139. panelRef,
  140. togglePane,
  141. handlePaneExpand,
  142. handlePaneCollapse,
  143. resizing,
  144. setResizing,
  145. activeModal,
  146. setActiveModal,
  147. openTab,
  148. ]
  149. )
  150. return <RailContext.Provider value={value}>{children}</RailContext.Provider>
  151. }
  152. export const useRailContext = () => {
  153. const context = useContext(RailContext)
  154. if (!context) {
  155. throw new Error('useRailContext is only available inside RailProvider')
  156. }
  157. return context
  158. }