Ver Fonte

[web] add Revert File button behind a feature flag (#17975)

* [web] add Revert File button behind a feature flag

* improve error message

* use constant for timeout

GitOrigin-RevId: 047c35d22e948351c52d469e48b869719f44ec4f
Domagoj Kriskovic há 2 anos atrás
pai
commit
945e51b8ed

+ 3 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -619,6 +619,9 @@ const ProjectController = {
         paywallCtaAssignment(cb) {
           SplitTestHandler.getAssignment(req, res, 'paywall-cta', cb)
         },
+        revertFileAssignment(cb) {
+          SplitTestHandler.getAssignment(req, res, 'revert-file', cb)
+        },
         projectTags(cb) {
           if (!userId) {
             return cb(null, [])

+ 4 - 0
services/web/frontend/extracted-translations.json

@@ -1047,7 +1047,11 @@
   "resync_project_history": "",
   "retry_test": "",
   "reverse_x_sort_order": "",
+  "revert_file": "",
+  "revert_file_error_message": "",
+  "revert_file_error_title": "",
   "revert_pending_plan_change": "",
+  "reverting": "",
   "review": "",
   "review_your_peers_work": "",
   "revoke": "",

+ 59 - 0
services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-revert-file-button.tsx

@@ -0,0 +1,59 @@
+import { Button, Modal } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import type { HistoryContextValue } from '../../../context/types/history-context-value'
+import { useRevertSelectedFile } from '@/features/history/context/hooks/use-revert-selected-file'
+import withErrorBoundary from '@/infrastructure/error-boundary'
+
+type ToolbarRevertingFileButtonProps = {
+  selection: HistoryContextValue['selection']
+}
+
+function ToolbarRevertFileButton({
+  selection,
+}: ToolbarRevertingFileButtonProps) {
+  const { t } = useTranslation()
+  const { revertSelectedFile, isLoading } = useRevertSelectedFile()
+
+  return (
+    <Button
+      className="btn-secondary history-react-toolbar-revert-file-button"
+      bsSize="xs"
+      bsStyle={null}
+      onClick={() => revertSelectedFile(selection)}
+      disabled={isLoading}
+    >
+      {isLoading ? `${t('reverting')}…` : t('revert_file')}
+    </Button>
+  )
+}
+
+function ToolbarRevertErrorModal({
+  resetErrorBoundary,
+}: {
+  resetErrorBoundary: VoidFunction
+}) {
+  const { t } = useTranslation()
+
+  return (
+    <Modal show onHide={resetErrorBoundary}>
+      <Modal.Header closeButton>
+        <Modal.Title>{t('revert_file_error_title')}</Modal.Title>
+      </Modal.Header>
+      <Modal.Body>{t('revert_file_error_message')}</Modal.Body>
+      <Modal.Footer>
+        <Button
+          bsStyle={null}
+          className="btn-secondary pull-left"
+          onClick={resetErrorBoundary}
+        >
+          {t('close')}
+        </Button>
+      </Modal.Footer>
+    </Modal>
+  )
+}
+
+export default withErrorBoundary(
+  ToolbarRevertFileButton,
+  ToolbarRevertErrorModal
+)

+ 12 - 0
services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar.tsx

@@ -5,6 +5,8 @@ import ToolbarDatetime from './toolbar-datetime'
 import ToolbarFileInfo from './toolbar-file-info'
 import ToolbarRestoreFileButton from './toolbar-restore-file-button'
 import { isFileRemoved } from '../../../utils/file-diff'
+import ToolbarRevertFileButton from './toolbar-revert-file-button'
+import { useSplitTestContext } from '@/shared/context/split-test-context'
 
 type ToolbarProps = {
   diff: Nullable<Diff>
@@ -12,9 +14,16 @@ type ToolbarProps = {
 }
 
 export default function Toolbar({ diff, selection }: ToolbarProps) {
+  const { splitTestVariants } = useSplitTestContext()
+
   const showRestoreFileButton =
     selection.selectedFile && isFileRemoved(selection.selectedFile)
 
+  const showRevertFileButton =
+    splitTestVariants['revert-file'] === 'enabled' &&
+    selection.selectedFile &&
+    !isFileRemoved(selection.selectedFile)
+
   return (
     <div className="history-react-toolbar">
       <ToolbarDatetime selection={selection} />
@@ -24,6 +33,9 @@ export default function Toolbar({ diff, selection }: ToolbarProps) {
       {showRestoreFileButton ? (
         <ToolbarRestoreFileButton selection={selection} />
       ) : null}
+      {showRevertFileButton ? (
+        <ToolbarRevertFileButton selection={selection} />
+      ) : null}
     </div>
   )
 }

+ 105 - 0
services/web/frontend/js/features/history/context/hooks/use-revert-selected-file.ts

@@ -0,0 +1,105 @@
+import { useIdeContext } from '../../../../shared/context/ide-context'
+import { useLayoutContext } from '../../../../shared/context/layout-context'
+import { revertFile } from '../../services/api'
+import { isFileRemoved } from '../../utils/file-diff'
+import { useHistoryContext } from '../history-context'
+import type { HistoryContextValue } from '../types/history-context-value'
+import { useErrorHandler } from 'react-error-boundary'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
+import { findInTree } from '@/features/file-tree/util/find-in-tree'
+import { useCallback, useEffect, useState } from 'react'
+import { RevertFileResponse } from '@/features/history/services/types/revert-file'
+
+const REVERT_FILE_TIMEOUT = 3000
+
+type RevertState =
+  | 'idle'
+  | 'reverting'
+  | 'waitingForFileTree'
+  | 'complete'
+  | 'error'
+  | 'timedOut'
+
+export function useRevertSelectedFile() {
+  const { projectId } = useHistoryContext()
+  const ide = useIdeContext()
+  const { setView } = useLayoutContext()
+  const handleError = useErrorHandler()
+  const { fileTreeData } = useFileTreeData()
+  const [state, setState] = useState<RevertState>('idle')
+  const [revertedFileMetadata, setRevertedFileMetadata] =
+    useState<RevertFileResponse | null>(null)
+
+  const isLoading = state === 'reverting' || state === 'waitingForFileTree'
+
+  useEffect(() => {
+    if (state === 'waitingForFileTree' && revertedFileMetadata) {
+      const result = findInTree(fileTreeData, revertedFileMetadata.id)
+      if (result) {
+        setState('complete')
+        const { _id: id } = result.entity
+        setView('editor')
+
+        // Once Angular is gone, these can be replaced with calls to context
+        // methods
+        if (revertedFileMetadata.type === 'doc') {
+          ide.editorManager.openDocId(id)
+        } else {
+          ide.binaryFilesManager.openFileWithId(id)
+        }
+      }
+    }
+  }, [
+    state,
+    fileTreeData,
+    revertedFileMetadata,
+    ide.editorManager,
+    ide.binaryFilesManager,
+    setView,
+  ])
+
+  useEffect(() => {
+    if (state === 'waitingForFileTree') {
+      const timer = window.setTimeout(() => {
+        setState('timedOut')
+        handleError(new Error('timed out'))
+      }, REVERT_FILE_TIMEOUT)
+
+      return () => {
+        window.clearTimeout(timer)
+      }
+    }
+  }, [handleError, state])
+
+  const revertSelectedFile = useCallback(
+    (selection: HistoryContextValue['selection']) => {
+      const { selectedFile, files } = selection
+
+      if (
+        selectedFile &&
+        selectedFile.pathname &&
+        !isFileRemoved(selectedFile)
+      ) {
+        const file = files.find(file => file.pathname === selectedFile.pathname)
+        const toVersion = selection.updateRange?.toV
+        if (file && !isFileRemoved(file) && toVersion) {
+          setState('reverting')
+
+          revertFile(projectId, file.pathname, toVersion).then(
+            (data: RevertFileResponse) => {
+              setRevertedFileMetadata(data)
+              setState('waitingForFileTree')
+            },
+            error => {
+              setState('error')
+              handleError(error)
+            }
+          )
+        }
+      }
+    },
+    [handleError, projectId]
+  )
+
+  return { revertSelectedFile, isLoading }
+}

+ 14 - 0
services/web/frontend/js/features/history/services/api.ts

@@ -8,6 +8,7 @@ import { FetchUpdatesResponse } from './types/update'
 import { Label } from './types/label'
 import { DocDiffResponse } from './types/doc'
 import { RestoreFileResponse } from './types/restore-file'
+import { RevertFileResponse } from './types/revert-file'
 
 const BATCH_SIZE = 10
 
@@ -94,3 +95,16 @@ export function restoreFile(projectId: string, selectedFile: FileRemoved) {
     },
   })
 }
+
+export function revertFile(
+  projectId: string,
+  pathname: string,
+  version: number
+) {
+  return postJSON<RevertFileResponse>(`/project/${projectId}/revert_file`, {
+    body: {
+      version,
+      pathname,
+    },
+  })
+}

+ 4 - 0
services/web/frontend/js/features/history/services/types/revert-file.ts

@@ -0,0 +1,4 @@
+export type RevertFileResponse = {
+  id: string
+  type: 'doc' | 'file'
+}

+ 4 - 0
services/web/locales/en.json

@@ -1546,7 +1546,11 @@
   "retry_test": "Retry test",
   "return_to_login_page": "Return to Login page",
   "reverse_x_sort_order": "Reverse __x__ sort order",
+  "revert_file": "Revert file",
+  "revert_file_error_message": "There was a problem reverting the file version. Please try again in a few moments. If the problem continues please contact us.",
+  "revert_file_error_title": "Revert File Error",
   "revert_pending_plan_change": "Revert scheduled plan change",
+  "reverting": "Reverting",
   "review": "Review",
   "review_your_peers_work": "Review your peers’ work",
   "revoke": "Revoke",