@@ -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 (
@@ -24,6 +33,9 @@ export default function Toolbar({ diff, selection }: ToolbarProps) {
{showRestoreFileButton ? (
) : null}
+ {showRevertFileButton ? (
+
+ ) : null}
)
}
diff --git a/services/web/frontend/js/features/history/context/hooks/use-revert-selected-file.ts b/services/web/frontend/js/features/history/context/hooks/use-revert-selected-file.ts
new file mode 100644
index 0000000000..d6bd2d5992
--- /dev/null
+++ b/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('idle')
+ const [revertedFileMetadata, setRevertedFileMetadata] =
+ useState(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 }
+}
diff --git a/services/web/frontend/js/features/history/services/api.ts b/services/web/frontend/js/features/history/services/api.ts
index e49d3af4bc..3da4e86380 100644
--- a/services/web/frontend/js/features/history/services/api.ts
+++ b/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(`/project/${projectId}/revert_file`, {
+ body: {
+ version,
+ pathname,
+ },
+ })
+}
diff --git a/services/web/frontend/js/features/history/services/types/revert-file.ts b/services/web/frontend/js/features/history/services/types/revert-file.ts
new file mode 100644
index 0000000000..1cc52a9f49
--- /dev/null
+++ b/services/web/frontend/js/features/history/services/types/revert-file.ts
@@ -0,0 +1,4 @@
+export type RevertFileResponse = {
+ id: string
+ type: 'doc' | 'file'
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 87a76e2ff0..638dbecfed 100644
--- a/services/web/locales/en.json
+++ b/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",