From 945e51b8edbbd276726846392cc007c43e7ab80f Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Thu, 18 Apr 2024 14:27:57 +0200 Subject: [PATCH] [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 --- .../src/Features/Project/ProjectController.js | 3 + .../web/frontend/extracted-translations.json | 4 + .../toolbar/toolbar-revert-file-button.tsx | 59 ++++++++++ .../components/diff-view/toolbar/toolbar.tsx | 12 ++ .../context/hooks/use-revert-selected-file.ts | 105 ++++++++++++++++++ .../js/features/history/services/api.ts | 14 +++ .../history/services/types/revert-file.ts | 4 + services/web/locales/en.json | 4 + 8 files changed, 205 insertions(+) create mode 100644 services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-revert-file-button.tsx create mode 100644 services/web/frontend/js/features/history/context/hooks/use-revert-selected-file.ts create mode 100644 services/web/frontend/js/features/history/services/types/revert-file.ts diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 966e02d586..7e868d6690 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/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, []) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index df9a0d9ebd..a9b10fa8a3 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-revert-file-button.tsx b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-revert-file-button.tsx new file mode 100644 index 0000000000..39a542b5ce --- /dev/null +++ b/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 ( + + ) +} + +function ToolbarRevertErrorModal({ + resetErrorBoundary, +}: { + resetErrorBoundary: VoidFunction +}) { + const { t } = useTranslation() + + return ( + + + {t('revert_file_error_title')} + + {t('revert_file_error_message')} + + + + + ) +} + +export default withErrorBoundary( + ToolbarRevertFileButton, + ToolbarRevertErrorModal +) diff --git a/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar.tsx b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar.tsx index 9f75b8f449..b5b1bca00f 100644 --- a/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar.tsx +++ b/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 @@ -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",