diff --git a/services/web/app/src/Features/Tutorial/TutorialController.js b/services/web/app/src/Features/Tutorial/TutorialController.js index 3cf1d6140f..0c01bcdfb6 100644 --- a/services/web/app/src/Features/Tutorial/TutorialController.js +++ b/services/web/app/src/Features/Tutorial/TutorialController.js @@ -9,6 +9,7 @@ const VALID_KEYS = [ 'bib-file-tpr-prompt', 'ai-error-assistant-consent', 'code-editor-mode-prompt', + 'history-restore-promo', ] async function completeTutorial(req, res, next) { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index c02cce0fa3..709b5f0153 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -614,6 +614,8 @@ "history_label_project_current_state": "", "history_label_this_version": "", "history_new_label_name": "", + "history_restore_promo_content": "", + "history_restore_promo_title": "", "history_resync": "", "history_view_a11y_description": "", "history_view_all": "", diff --git a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx index 6cc979685b..2c0abd1a2f 100644 --- a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx +++ b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import HistoryVersion from './history-version' import LoadingSpinner from '../../../../shared/components/loading-spinner' import { OwnerPaywallPrompt } from './owner-paywall-prompt' @@ -12,9 +12,8 @@ import { Overlay, Popover } from 'react-bootstrap' import Close from '@/shared/components/close' import { Trans, useTranslation } from 'react-i18next' import MaterialIcon from '@/shared/components/material-icon' -import useAsync from '@/shared/hooks/use-async' -import { completeHistoryTutorial } from '../../services/api' -import { debugConsole } from '@/utils/debugging' +import useTutorial from '@/shared/hooks/promotions/use-tutorial' +import { useFeatureFlag } from '@/shared/context/split-test-context' function AllHistoryList() { const { id: currentUserId } = useUserContext() @@ -92,23 +91,29 @@ function AllHistoryList() { } }, [updatesLoadingState]) - const { inactiveTutorials, deactivateTutorial } = useEditorContext() - - const [showPopover, setShowPopover] = useState(() => { - // only show tutorial popover if they haven't dismissed ("completed") it yet - return !inactiveTutorials.includes('react-history-buttons-tutorial') + const { inactiveTutorials } = useEditorContext() + const { + showPopup: showHistoryTutorial, + tryShowingPopup: tryShowingHistoryTutorial, + hideUntilReload: hideHistoryTutorialUntilReload, + completeTutorial: completeHistoryTutorial, + } = useTutorial('react-history-buttons-tutorial', { + name: 'react-history-buttons-tutorial', }) - const completeTutorial = useCallback(() => { - setShowPopover(false) - deactivateTutorial('react-history-buttons-tutorial') - }, [deactivateTutorial]) + const { + showPopup: showRestorePromo, + tryShowingPopup: tryShowingRestorePromo, + hideUntilReload: hideRestorePromoUntilReload, + completeTutorial: completeRestorePromo, + } = useTutorial('history-restore-promo', { + name: 'history-restore-promo', + }) + const inFileRestoreSplitTest = useFeatureFlag('revert-file') + const inProjectRestoreSplitTest = useFeatureFlag('revert-project') - const { runAsync } = useAsync() - - const { t } = useTranslation() - - // wait for the layout to settle before showing popover, to avoid a flash/ instant move + const hasVisibleUpdates = visibleUpdates.length > 0 + const isMoreThanOneVersion = visibleUpdates.length > 1 const [layoutSettled, setLayoutSettled] = useState(false) // When there is a paywall and only two version's to compare, @@ -117,30 +122,60 @@ function AllHistoryList() { const isPaywallAndNonComparable = visibleUpdates.length === 2 && updatesInfo.freeHistoryLimitHit - const isMoreThanOneVersion = visibleUpdates.length > 1 + useEffect(() => { + const hasCompletedHistoryTutorial = inactiveTutorials.includes( + 'react-history-buttons-tutorial' + ) + const hasCompletedRestorePromotion = inactiveTutorials.includes( + 'history-restore-promo' + ) + + // wait for the layout to settle before showing popover, to avoid a flash/ instant move + if (!layoutSettled) { + return + } + if ( + !hasCompletedHistoryTutorial && + isMoreThanOneVersion && + !isPaywallAndNonComparable + ) { + tryShowingHistoryTutorial() + } else if ( + !hasCompletedRestorePromotion && + inFileRestoreSplitTest && + inProjectRestoreSplitTest && + hasVisibleUpdates + ) { + tryShowingRestorePromo() + } + }, [ + hasVisibleUpdates, + inFileRestoreSplitTest, + inProjectRestoreSplitTest, + tryShowingRestorePromo, + inactiveTutorials, + isMoreThanOneVersion, + isPaywallAndNonComparable, + layoutSettled, + tryShowingHistoryTutorial, + ]) + + const { t } = useTranslation() + let popover = null // hiding is different from dismissing, as we wont save a full dismissal to the user // meaning the tutorial will show on page reload/ re-navigation const hidePopover = () => { - completeTutorial() + hideHistoryTutorialUntilReload() + hideRestorePromoUntilReload() } - if ( - isMoreThanOneVersion && - showPopover && - !isPaywallAndNonComparable && - layoutSettled - ) { - const dismissModal = () => { - completeTutorial() - runAsync(completeHistoryTutorial()).catch(debugConsole.error) - } - + if (showHistoryTutorial) { popover = ( {t('react_history_tutorial_title')}{' '} - dismissModal()} /> + + completeHistoryTutorial({ + event: 'promo-click', + action: 'complete', + }) + } + /> } className="dark-themed history-popover" @@ -172,6 +215,49 @@ function AllHistoryList() { ) + } else if (showRestorePromo) { + popover = ( + + + {t('history_restore_promo_title')} + + completeRestorePromo({ + event: 'promo-click', + action: 'complete', + }) + } + /> + + } + className="dark-themed history-popover" + > + , + ]} + /> + + + ) } // give the components time to position before showing popover so we don't get an instant position change diff --git a/services/web/frontend/js/features/history/services/api.ts b/services/web/frontend/js/features/history/services/api.ts index 91d2573dab..a9c926dc86 100644 --- a/services/web/frontend/js/features/history/services/api.ts +++ b/services/web/frontend/js/features/history/services/api.ts @@ -50,10 +50,6 @@ export function deleteLabel( return deleteJSON(`/project/${projectId}/labels/${labelId}`, { signal }) } -export function completeHistoryTutorial() { - return postJSON('/tutorial/react-history-buttons-tutorial/complete') -} - export function diffFiles( projectId: string, fromV: number, diff --git a/services/web/frontend/js/shared/hooks/promotions/use-tutorial.tsx b/services/web/frontend/js/shared/hooks/promotions/use-tutorial.tsx index 5b898fbe5f..2d5e814b5c 100644 --- a/services/web/frontend/js/shared/hooks/promotions/use-tutorial.tsx +++ b/services/web/frontend/js/shared/hooks/promotions/use-tutorial.tsx @@ -81,6 +81,11 @@ export const useTutorial = ( [setCurrentPopup, setShowPopup, tutorialKey, eventData] ) + const hideUntilReload = useCallback(() => { + clearPopup() + deactivateTutorial(tutorialKey) + }, [clearPopup, deactivateTutorial, tutorialKey]) + return { completeTutorial, dismissTutorial, @@ -89,6 +94,7 @@ export const useTutorial = ( clearPopup, clearAndShow, showPopup, + hideUntilReload, } } diff --git a/services/web/frontend/stylesheets/app/editor/history-react.less b/services/web/frontend/stylesheets/app/editor/history-react.less index 372e368f93..f000c3b6b6 100644 --- a/services/web/frontend/stylesheets/app/editor/history-react.less +++ b/services/web/frontend/stylesheets/app/editor/history-react.less @@ -427,6 +427,9 @@ history-root { color: @neutral-10; vertical-align: top; } +.history-restore-promo-icon { + vertical-align: middle; +} .history-file-tree { display: flex !important; // To work around jQuery layout's inline styles diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 7724a8722d..ab8dd86ed9 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -870,6 +870,8 @@ "history_label_project_current_state": "Current state", "history_label_this_version": "Label this version", "history_new_label_name": "New label name", + "history_restore_promo_content": "Now you can restore a single file or your whole project to a previous version, including comments and tracked changes. Click Restore this version to restore the selected file or use the <0> menu in the history entry to restore the full project.", + "history_restore_promo_title": "Need to turn back time?", "history_resync": "History resync", "history_view_a11y_description": "Show all of the project history or only labelled versions.", "history_view_all": "All history",