rail-context.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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 =
  27. | 'keyboard-shortcuts'
  28. | 'contact-us'
  29. | 'dictionary'
  30. | 'labs'
  31. export function dispatchOpenRailModal(key: RailModalKey) {
  32. window.dispatchEvent(new CustomEvent('ui:open-rail-modal', { detail: key }))
  33. }
  34. const RailContext = createContext<
  35. | {
  36. selectedTab: RailTabKey
  37. isOpen: boolean
  38. setIsOpen: Dispatch<SetStateAction<boolean>>
  39. panelRef: React.RefObject<ImperativePanelHandle>
  40. togglePane: () => void
  41. handlePaneExpand: () => void
  42. handlePaneCollapse: () => void
  43. resizing: boolean
  44. setResizing: Dispatch<SetStateAction<boolean>>
  45. activeModal: RailModalKey | null
  46. setActiveModal: Dispatch<SetStateAction<RailModalKey | null>>
  47. openTab: (tab: RailTabKey) => void
  48. selectTab: (tab: RailTabKey) => void
  49. }
  50. | undefined
  51. >(undefined)
  52. export const RailProvider: FC<React.PropsWithChildren> = ({ children }) => {
  53. const { projectId } = useProjectContext()
  54. const [isOpen, setIsOpen] = usePersistedState(
  55. `rail-is-open-${projectId}`,
  56. true
  57. )
  58. const [resizing, setResizing] = useState(false)
  59. const [activeModal, setActiveModalInternal] = useState<RailModalKey | null>(
  60. null
  61. )
  62. const setActiveModal: Dispatch<SetStateAction<RailModalKey | null>> =
  63. useCallback(modalKey => {
  64. setActiveModalInternal(modalKey)
  65. }, [])
  66. const panelRef = useRef<ImperativePanelHandle>(null)
  67. const togglePane = useCallback(() => {
  68. setIsOpen(value => !value)
  69. }, [setIsOpen])
  70. const handlePaneExpand = useCallback(() => {
  71. setIsOpen(true)
  72. }, [setIsOpen])
  73. const handlePaneCollapse = useCallback(() => {
  74. setIsOpen(false)
  75. }, [setIsOpen])
  76. const [selectedTab, setSelectedTab] = usePersistedState<RailTabKey>(
  77. `selected-rail-tab-${projectId}`,
  78. 'file-tree'
  79. )
  80. // Keep the panel collapse/expanded state in sync with isOpen and selectedTab
  81. useLayoutEffect(() => {
  82. const panelHandle = panelRef.current
  83. if (panelHandle) {
  84. if (isOpen) {
  85. panelHandle.expand()
  86. } else {
  87. panelHandle.collapse()
  88. }
  89. }
  90. }, [isOpen, selectedTab])
  91. const selectTab = useCallback(
  92. (tab: RailTabKey) => {
  93. setSelectedTab(tab)
  94. },
  95. [setSelectedTab]
  96. )
  97. const openTab = useCallback(
  98. (tab: RailTabKey) => {
  99. setSelectedTab(tab)
  100. setIsOpen(true)
  101. },
  102. [setIsOpen, setSelectedTab]
  103. )
  104. useEventListener(
  105. 'ui.toggle-review-panel',
  106. useCallback(() => {
  107. if (isOpen && selectedTab === 'review-panel') {
  108. handlePaneCollapse()
  109. } else {
  110. openTab('review-panel')
  111. }
  112. }, [handlePaneCollapse, selectedTab, isOpen, openTab])
  113. )
  114. useEventListener(
  115. 'ui:open-rail-modal',
  116. useCallback(
  117. (event: Event) => {
  118. setActiveModal((event as CustomEvent<RailModalKey>).detail)
  119. },
  120. [setActiveModal]
  121. )
  122. )
  123. useEventListener(
  124. 'keydown',
  125. useCallback(
  126. (event: KeyboardEvent) => {
  127. if (
  128. (isMac ? event.metaKey : event.ctrlKey) &&
  129. event.shiftKey &&
  130. event.code === 'KeyF'
  131. ) {
  132. event.preventDefault()
  133. sendSearchEvent('search-open', {
  134. searchType: 'full-project',
  135. method: 'keyboard',
  136. })
  137. openTab('full-project-search')
  138. }
  139. },
  140. [openTab]
  141. )
  142. )
  143. const value = useMemo(
  144. () => ({
  145. selectedTab,
  146. isOpen,
  147. setIsOpen,
  148. panelRef,
  149. togglePane,
  150. handlePaneExpand,
  151. handlePaneCollapse,
  152. resizing,
  153. setResizing,
  154. activeModal,
  155. setActiveModal,
  156. openTab,
  157. selectTab,
  158. }),
  159. [
  160. selectedTab,
  161. isOpen,
  162. setIsOpen,
  163. panelRef,
  164. togglePane,
  165. handlePaneExpand,
  166. handlePaneCollapse,
  167. resizing,
  168. setResizing,
  169. activeModal,
  170. setActiveModal,
  171. openTab,
  172. selectTab,
  173. ]
  174. )
  175. return <RailContext.Provider value={value}>{children}</RailContext.Provider>
  176. }
  177. export const useRailContext = () => {
  178. const context = useContext(RailContext)
  179. if (!context) {
  180. throw new Error('useRailContext is only available inside RailProvider')
  181. }
  182. return context
  183. }