Merge pull request #20233 from overleaf/mj-restore-promo

[web] Add promotion for file/project reverting

GitOrigin-RevId: 9f8e66ab2ad945274576800253d288bca5986562
This commit is contained in:
Mathias Jakobsen 2024-09-06 10:20:42 +01:00 committed by Copybot
parent 8245a95b4e
commit 7d80d22e96
7 changed files with 133 additions and 37 deletions

View file

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

View file

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

View file

@ -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 = (
<Overlay
placement="left"
show={showPopover}
show={showHistoryTutorial}
rootClose
onHide={hidePopover}
// using scrollerRef to position the popover in the middle of the viewport
@ -153,7 +188,15 @@ function AllHistoryList() {
title={
<span>
{t('react_history_tutorial_title')}{' '}
<Close variant="dark" onDismiss={() => dismissModal()} />
<Close
variant="dark"
onDismiss={() =>
completeHistoryTutorial({
event: 'promo-click',
action: 'complete',
})
}
/>
</span>
}
className="dark-themed history-popover"
@ -172,6 +215,49 @@ function AllHistoryList() {
</Popover>
</Overlay>
)
} else if (showRestorePromo) {
popover = (
<Overlay
placement="left"
show={showRestorePromo}
rootClose
onHide={hidePopover}
// using scrollerRef to position the popover in the middle of the viewport
target={scrollerRef.current ?? undefined}
shouldUpdatePosition
>
<Popover
id="popover-toolbar-overflow"
arrowOffsetTop={10}
title={
<span>
{t('history_restore_promo_title')}
<Close
variant="dark"
onDismiss={() =>
completeRestorePromo({
event: 'promo-click',
action: 'complete',
})
}
/>
</span>
}
className="dark-themed history-popover"
>
<Trans
i18nKey="history_restore_promo_content"
components={[
// eslint-disable-next-line react/jsx-key
<MaterialIcon
type="more_vert"
className="history-restore-promo-icon"
/>,
]}
/>
</Popover>
</Overlay>
)
}
// give the components time to position before showing popover so we don't get an instant position change

View file

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

View file

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

View file

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

View file

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