[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
This commit is contained in:
Domagoj Kriskovic 2024-04-18 14:27:57 +02:00 committed by Copybot
parent 5ad70690c9
commit 945e51b8ed
8 changed files with 205 additions and 0 deletions

View file

@ -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, [])

View file

@ -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": "",

View file

@ -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
)

View file

@ -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>
)
}

View file

@ -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 }
}

View file

@ -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,
},
})
}

View file

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

View file

@ -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",