mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-09 00:22:38 +00:00
Merge pull request #20233 from overleaf/mj-restore-promo
[web] Add promotion for file/project reverting GitOrigin-RevId: 9f8e66ab2ad945274576800253d288bca5986562
This commit is contained in:
parent
8245a95b4e
commit
7d80d22e96
7 changed files with 133 additions and 37 deletions
|
@ -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) {
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue