Explorar o código

Merge pull request #25127 from overleaf/mj-full-project-search-promotion

[web] Add promotion for full-project-search

GitOrigin-RevId: e102dbf7df8b63afc592c57ebf6dafa51efdf9ff
Mathias Jakobsen hai 1 ano
pai
achega
7a072164a2

+ 1 - 0
services/web/app/src/Features/Tutorial/TutorialController.mjs

@@ -11,6 +11,7 @@ const VALID_KEYS = [
   'history-restore-promo',
   'us-gov-banner',
   'us-gov-banner-fedramp',
+  'full-project-search-promo',
   'editor-popup-ux-survey',
 ]
 

+ 1 - 0
services/web/frontend/extracted-translations.json

@@ -1104,6 +1104,7 @@
   "notification_personal_and_group_subscriptions": "",
   "notification_project_invite_accepted_message": "",
   "notification_project_invite_message": "",
+  "now_you_can_search_your_whole_project_not_just_this_file": "",
   "number_of_users": "",
   "numbered_list": "",
   "oauth_orcid_description": "",

+ 4 - 36
services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx

@@ -33,12 +33,12 @@ import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { useTranslation } from 'react-i18next'
 import classnames from 'classnames'
 import { useUserSettingsContext } from '@/shared/context/user-settings-context'
-import { useLayoutContext } from '@/shared/context/layout-context'
 import { getStoredSelection, setStoredSelection } from '../extensions/search'
 import { debounce } from 'lodash'
 import { EditorSelection, EditorState } from '@codemirror/state'
 import { sendSearchEvent } from '@/features/event-tracking/search-events'
 import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
+import { FullProjectSearchButton } from './full-project-search-button'
 
 const MATCH_COUNT_DEBOUNCE_WAIT = 100 // the amount of ms to wait before counting matches
 const MAX_MATCH_COUNT = 999 // the maximum number of matches to count
@@ -60,7 +60,6 @@ type MatchPositions = {
 const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
   const view = useCodeMirrorViewContext()
   const state = useCodeMirrorStateContext()
-  const { setProjectSearchIsOpen } = useLayoutContext()
 
   const { userSettings } = useUserSettingsContext()
   const emacsKeybindingsActive = userSettings.mode === 'emacs'
@@ -244,16 +243,6 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
     return getSearchQuery(state)
   }, [state])
 
-  const openFullProjectSearch = useCallback(() => {
-    setProjectSearchIsOpen(true)
-    closeSearchPanel(view)
-    window.setTimeout(() => {
-      window.dispatchEvent(
-        new CustomEvent('editor:full-project-search', { detail: query })
-      )
-    }, 200)
-  }, [setProjectSearchIsOpen, query, view])
-
   const showReplace = !state.readOnly
 
   return (
@@ -455,30 +444,9 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
             </OLButton>
           </OLButtonGroup>
 
-          {!newEditor && isSplitTestEnabled('full-project-search') ? (
-            <OLTooltip
-              id="open-full-project-search"
-              description={t('search_all_project_files')}
-            >
-              <OLButton
-                variant="secondary"
-                size="sm"
-                onClick={() => {
-                  sendSearchEvent('search-open', {
-                    searchType: 'full-project',
-                    method: 'button',
-                    location: 'search-form',
-                  })
-                  openFullProjectSearch()
-                }}
-              >
-                <MaterialIcon
-                  type="manage_search"
-                  accessibilityLabel={t('search_next')}
-                />
-              </OLButton>
-            </OLTooltip>
-          ) : null}
+          {!newEditor && isSplitTestEnabled('full-project-search') && (
+            <FullProjectSearchButton query={query} />
+          )}
 
           {position !== null && (
             <div className="ol-cm-search-form-position">

+ 146 - 0
services/web/frontend/js/features/source-editor/components/full-project-search-button.tsx

@@ -0,0 +1,146 @@
+import { sendSearchEvent } from '@/features/event-tracking/search-events'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import { useLayoutContext } from '@/shared/context/layout-context'
+import { closeSearchPanel, SearchQuery } from '@codemirror/search'
+import { forwardRef, memo, Ref, useCallback, useEffect, useRef } from 'react'
+import { useCodeMirrorViewContext } from './codemirror-context'
+import MaterialIcon from '@/shared/components/material-icon'
+import { useTranslation } from 'react-i18next'
+import { Overlay, Popover } from 'react-bootstrap-5'
+import Close from '@/shared/components/close'
+import useTutorial from '@/shared/hooks/promotions/use-tutorial'
+import { useEditorContext } from '@/shared/context/editor-context'
+
+export const FullProjectSearchButton = ({ query }: { query: SearchQuery }) => {
+  const view = useCodeMirrorViewContext()
+  const { t } = useTranslation()
+  const { setProjectSearchIsOpen } = useLayoutContext()
+  const ref = useRef<HTMLButtonElement>(null)
+
+  const { inactiveTutorials } = useEditorContext()
+
+  const hasCompletedTutorial = inactiveTutorials.includes(
+    'full-project-search-promo'
+  )
+
+  const { showPopup, tryShowingPopup, hideUntilReload, completeTutorial } =
+    useTutorial('full-project-search-promo', {
+      name: 'full-project-search-promotion',
+    })
+
+  const openFullProjectSearch = useCallback(() => {
+    setProjectSearchIsOpen(true)
+    closeSearchPanel(view)
+    window.setTimeout(() => {
+      window.dispatchEvent(
+        new CustomEvent('editor:full-project-search', { detail: query })
+      )
+    }, 200)
+  }, [setProjectSearchIsOpen, query, view])
+
+  const onClick = useCallback(() => {
+    sendSearchEvent('search-open', {
+      searchType: 'full-project',
+      method: 'button',
+      location: 'search-form',
+    })
+    openFullProjectSearch()
+    if (!hasCompletedTutorial) {
+      completeTutorial({ action: 'complete', event: 'promo-click' })
+    }
+  }, [completeTutorial, openFullProjectSearch, hasCompletedTutorial])
+
+  return (
+    <>
+      <OLTooltip
+        id="open-full-project-search"
+        overlayProps={{ placement: 'bottom' }}
+        description={t('search_all_project_files')}
+      >
+        <OLButton variant="secondary" size="sm" ref={ref} onClick={onClick}>
+          <MaterialIcon
+            type="manage_search"
+            accessibilityLabel={t('search_next')}
+          />
+        </OLButton>
+      </OLTooltip>
+      {!hasCompletedTutorial && (
+        <PromotionOverlay
+          ref={ref}
+          showPopup={showPopup}
+          tryShowingPopup={tryShowingPopup}
+          completeTutorial={completeTutorial}
+          hideUntilReload={hideUntilReload}
+        />
+      )}
+    </>
+  )
+}
+
+type PromotionOverlayProps = {
+  showPopup: boolean
+  tryShowingPopup: () => void
+  completeTutorial: (event: {
+    action: 'complete'
+    event: 'promo-dismiss'
+  }) => void
+  hideUntilReload: () => void
+}
+
+const PromotionOverlay = forwardRef<HTMLButtonElement, PromotionOverlayProps>(
+  function PromotionOverlay(
+    props: PromotionOverlayProps,
+    ref: Ref<HTMLButtonElement>
+  ) {
+    if (typeof ref === 'function' || !ref?.current) {
+      return null
+    }
+
+    return <PromotionContent target={ref.current} {...props} />
+  }
+)
+
+const PromotionContent = memo(function PromotionContent({
+  showPopup,
+  tryShowingPopup,
+  completeTutorial,
+  hideUntilReload,
+  target,
+}: PromotionOverlayProps & {
+  target: HTMLButtonElement
+}) {
+  const { t } = useTranslation()
+
+  useEffect(() => {
+    tryShowingPopup()
+  }, [tryShowingPopup])
+
+  const onHide = useCallback(() => {
+    hideUntilReload()
+  }, [hideUntilReload])
+
+  const onClose = useCallback(() => {
+    completeTutorial({
+      action: 'complete',
+      event: 'promo-dismiss',
+    })
+  }, [completeTutorial])
+
+  return (
+    <Overlay
+      placement="top"
+      show={showPopup}
+      target={target}
+      rootClose
+      onHide={onHide}
+    >
+      <Popover>
+        <Popover.Body>
+          <Close variant="dark" onDismiss={onClose} />
+          {t('now_you_can_search_your_whole_project_not_just_this_file')}
+        </Popover.Body>
+      </Popover>
+    </Overlay>
+  )
+})

+ 1 - 0
services/web/locales/en.json

@@ -1453,6 +1453,7 @@
   "notification_project_invite_accepted_message": "You’ve joined <b>__projectName__</b>",
   "notification_project_invite_message": "<b>__userName__</b> would like you to join <b>__projectName__</b>",
   "november": "November",
+  "now_you_can_search_your_whole_project_not_just_this_file": "Now you can search your whole project (not just this file!)",
   "number_collab": "Number of collaborators",
   "number_collab_info": "The number of people you can invite to work on a project with you. The limit is per project, so you can invite different people to each project.",
   "number_of_projects": "Number of projects",