Răsfoiți Sursa

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

[web] Add promotion for file/project reverting

GitOrigin-RevId: 9f8e66ab2ad945274576800253d288bca5986562
Mathias Jakobsen 1 an în urmă
părinte
comite
7d80d22e96

+ 1 - 0
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) {

+ 2 - 0
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": "",

+ 119 - 33
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 { runAsync } = useAsync()
-
-  const { t } = useTranslation()
+  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')
 
-  // 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

+ 0 - 4
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,

+ 6 - 0
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,
   }
 }
 

+ 3 - 0
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

+ 2 - 0
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></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",