rail-context.tsx 4.7 KB

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