full-project-search-button.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { sendSearchEvent } from '@/features/event-tracking/search-events'
  2. import OLButton from '@/features/ui/components/ol/ol-button'
  3. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  4. import { useLayoutContext } from '@/shared/context/layout-context'
  5. import { closeSearchPanel, SearchQuery } from '@codemirror/search'
  6. import { forwardRef, memo, Ref, useCallback, useEffect, useRef } from 'react'
  7. import { useCodeMirrorViewContext } from './codemirror-context'
  8. import MaterialIcon from '@/shared/components/material-icon'
  9. import { useTranslation } from 'react-i18next'
  10. import { Overlay, Popover } from 'react-bootstrap'
  11. import Close from '@/shared/components/close'
  12. import useTutorial from '@/shared/hooks/promotions/use-tutorial'
  13. import { useEditorContext } from '@/shared/context/editor-context'
  14. import getMeta from '@/utils/meta'
  15. import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
  16. import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
  17. const PROMOTION_SIGNUP_CUT_OFF_DATE = new Date('2025-04-22T00:00:00Z')
  18. export const FullProjectSearchButton = ({ query }: { query: SearchQuery }) => {
  19. const view = useCodeMirrorViewContext()
  20. const { t } = useTranslation()
  21. const { setProjectSearchIsOpen } = useLayoutContext()
  22. const newEditor = useIsNewEditorEnabled()
  23. const { openTab } = useRailContext()
  24. const ref = useRef<HTMLButtonElement>(null)
  25. const { inactiveTutorials } = useEditorContext()
  26. const hasCompletedTutorial = inactiveTutorials.includes(
  27. 'full-project-search-promo'
  28. )
  29. const { showPopup, tryShowingPopup, hideUntilReload, completeTutorial } =
  30. useTutorial('full-project-search-promo', {
  31. name: 'full-project-search-promotion',
  32. })
  33. let isEligibleForPromotion = true
  34. const signUpDateString = getMeta('ol-user')?.signUpDate
  35. if (!signUpDateString) {
  36. isEligibleForPromotion = false
  37. } else {
  38. const signupDate = new Date(signUpDateString)
  39. if (signupDate > PROMOTION_SIGNUP_CUT_OFF_DATE) {
  40. isEligibleForPromotion = false
  41. }
  42. }
  43. const openFullProjectSearch = useCallback(() => {
  44. if (newEditor) {
  45. openTab('full-project-search')
  46. } else {
  47. setProjectSearchIsOpen(true)
  48. }
  49. closeSearchPanel(view)
  50. window.setTimeout(() => {
  51. window.dispatchEvent(
  52. new CustomEvent('editor:full-project-search', { detail: query })
  53. )
  54. }, 200)
  55. }, [setProjectSearchIsOpen, query, view, newEditor, openTab])
  56. const onClick = useCallback(() => {
  57. sendSearchEvent('search-open', {
  58. searchType: 'full-project',
  59. method: 'button',
  60. location: 'search-form',
  61. })
  62. openFullProjectSearch()
  63. if (!hasCompletedTutorial && isEligibleForPromotion) {
  64. completeTutorial({ action: 'complete', event: 'promo-click' })
  65. }
  66. }, [
  67. completeTutorial,
  68. openFullProjectSearch,
  69. hasCompletedTutorial,
  70. isEligibleForPromotion,
  71. ])
  72. return (
  73. <>
  74. <OLTooltip
  75. id="open-full-project-search"
  76. overlayProps={{ placement: 'bottom' }}
  77. description={t('search_all_project_files')}
  78. >
  79. <OLButton variant="secondary" size="sm" ref={ref} onClick={onClick}>
  80. <MaterialIcon
  81. type="manage_search"
  82. accessibilityLabel={t('search_all_project_files')}
  83. />
  84. </OLButton>
  85. </OLTooltip>
  86. {!hasCompletedTutorial && isEligibleForPromotion && (
  87. <PromotionOverlay
  88. ref={ref}
  89. showPopup={showPopup}
  90. tryShowingPopup={tryShowingPopup}
  91. completeTutorial={completeTutorial}
  92. hideUntilReload={hideUntilReload}
  93. />
  94. )}
  95. </>
  96. )
  97. }
  98. type PromotionOverlayProps = {
  99. showPopup: boolean
  100. tryShowingPopup: () => void
  101. completeTutorial: (event: {
  102. action: 'complete'
  103. event: 'promo-dismiss'
  104. }) => void
  105. hideUntilReload: () => void
  106. }
  107. const PromotionOverlay = forwardRef<HTMLButtonElement, PromotionOverlayProps>(
  108. function PromotionOverlay(
  109. props: PromotionOverlayProps,
  110. ref: Ref<HTMLButtonElement>
  111. ) {
  112. if (typeof ref === 'function' || !ref?.current) {
  113. return null
  114. }
  115. return <PromotionContent target={ref.current} {...props} />
  116. }
  117. )
  118. const PromotionContent = memo(function PromotionContent({
  119. showPopup,
  120. tryShowingPopup,
  121. completeTutorial,
  122. hideUntilReload,
  123. target,
  124. }: PromotionOverlayProps & {
  125. target: HTMLButtonElement
  126. }) {
  127. const { t } = useTranslation()
  128. useEffect(() => {
  129. tryShowingPopup()
  130. }, [tryShowingPopup])
  131. const onHide = useCallback(() => {
  132. hideUntilReload()
  133. }, [hideUntilReload])
  134. const onClose = useCallback(() => {
  135. completeTutorial({
  136. action: 'complete',
  137. event: 'promo-dismiss',
  138. })
  139. }, [completeTutorial])
  140. return (
  141. <Overlay
  142. placement="top"
  143. show={showPopup}
  144. target={target}
  145. rootClose
  146. onHide={onHide}
  147. >
  148. <Popover>
  149. <Popover.Body>
  150. <Close variant="dark" onDismiss={onClose} />
  151. {t('now_you_can_search_your_whole_project_not_just_this_file')}
  152. </Popover.Body>
  153. </Popover>
  154. </Overlay>
  155. )
  156. })