diff --git a/services/web/app/src/Features/Tutorial/TutorialController.mjs b/services/web/app/src/Features/Tutorial/TutorialController.mjs index a8d5ab3145..a5cdf8d478 100644 --- a/services/web/app/src/Features/Tutorial/TutorialController.mjs +++ b/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', ] diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index b82b758f85..7df8553f55 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx index 61aeda839d..c013dcdb42 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx +++ b/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 = () => { const view = useCodeMirrorViewContext() const state = useCodeMirrorStateContext() - const { setProjectSearchIsOpen } = useLayoutContext() const { userSettings } = useUserSettingsContext() const emacsKeybindingsActive = userSettings.mode === 'emacs' @@ -244,16 +243,6 @@ const CodeMirrorSearchForm: FC = () => { 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 = () => { - {!newEditor && isSplitTestEnabled('full-project-search') ? ( - - { - sendSearchEvent('search-open', { - searchType: 'full-project', - method: 'button', - location: 'search-form', - }) - openFullProjectSearch() - }} - > - - - - ) : null} + {!newEditor && isSplitTestEnabled('full-project-search') && ( + + )} {position !== null && (
diff --git a/services/web/frontend/js/features/source-editor/components/full-project-search-button.tsx b/services/web/frontend/js/features/source-editor/components/full-project-search-button.tsx new file mode 100644 index 0000000000..6244067fc3 --- /dev/null +++ b/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(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 ( + <> + + + + + + {!hasCompletedTutorial && ( + + )} + + ) +} + +type PromotionOverlayProps = { + showPopup: boolean + tryShowingPopup: () => void + completeTutorial: (event: { + action: 'complete' + event: 'promo-dismiss' + }) => void + hideUntilReload: () => void +} + +const PromotionOverlay = forwardRef( + function PromotionOverlay( + props: PromotionOverlayProps, + ref: Ref + ) { + if (typeof ref === 'function' || !ref?.current) { + return null + } + + return + } +) + +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 ( + + + + + {t('now_you_can_search_your_whole_project_not_just_this_file')} + + + + ) +}) diff --git a/services/web/locales/en.json b/services/web/locales/en.json index b77691d277..389e3078b5 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1453,6 +1453,7 @@ "notification_project_invite_accepted_message": "You’ve joined __projectName__", "notification_project_invite_message": "__userName__ would like you to join __projectName__", "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",