full-project-search-button.tsx 4.6 KB

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