mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
[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:
parent
5ad70690c9
commit
945e51b8ed
8 changed files with 205 additions and 0 deletions
|
@ -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, [])
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export type RevertFileResponse = {
|
||||
id: string
|
||||
type: 'doc' | '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",
|
||||
|
|
Loading…
Reference in a new issue