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

[web] Add promotion for full-project-search

GitOrigin-RevId: e102dbf7df8b63afc592c57ebf6dafa51efdf9ff
This commit is contained in:
Mathias Jakobsen
2025-05-02 09:35:43 +01:00
committed by Copybot
parent 18f2aa5a0c
commit 75868a5454
5 changed files with 153 additions and 36 deletions

View File

@@ -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',
]

View File

@@ -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": "",

View File

@@ -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">

View File

@@ -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>
)
})

View File

@@ -1453,6 +1453,7 @@
"notification_project_invite_accepted_message": "Youve 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",