first commit
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import Toolbar from './toolbar/toolbar'
|
||||
import Main from './main'
|
||||
import { Diff, DocDiffResponse } from '../../services/types/doc'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
import { diffDoc } from '../../services/api'
|
||||
import { highlightsFromDiffResponse } from '../../utils/highlights-from-diff-response'
|
||||
import { useErrorHandler } from 'react-error-boundary'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function DiffView() {
|
||||
const { selection, projectId, loadingFileDiffs } = useHistoryContext()
|
||||
const { isLoading, data, runAsync } = useAsync<DocDiffResponse>()
|
||||
const { t } = useTranslation()
|
||||
const { updateRange, selectedFile } = selection
|
||||
const handleError = useErrorHandler()
|
||||
|
||||
useEffect(() => {
|
||||
if (!updateRange || !selectedFile?.pathname || loadingFileDiffs) {
|
||||
return
|
||||
}
|
||||
|
||||
const { fromV, toV } = updateRange
|
||||
let abortController: AbortController | null = new AbortController()
|
||||
|
||||
runAsync(
|
||||
diffDoc(
|
||||
projectId,
|
||||
fromV,
|
||||
toV,
|
||||
selectedFile.pathname,
|
||||
abortController.signal
|
||||
)
|
||||
)
|
||||
.catch(handleError)
|
||||
.finally(() => {
|
||||
abortController = null
|
||||
})
|
||||
|
||||
// Abort an existing request before starting a new one or on unmount
|
||||
return () => {
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
}
|
||||
}
|
||||
}, [
|
||||
projectId,
|
||||
runAsync,
|
||||
updateRange,
|
||||
selectedFile,
|
||||
loadingFileDiffs,
|
||||
handleError,
|
||||
])
|
||||
|
||||
const diff = useMemo(() => {
|
||||
let diff: Diff | null
|
||||
|
||||
if (!data?.diff) {
|
||||
diff = null
|
||||
} else if ('binary' in data.diff) {
|
||||
diff = { binary: true }
|
||||
} else {
|
||||
diff = {
|
||||
binary: false,
|
||||
docDiff: highlightsFromDiffResponse(data.diff, t),
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}, [data, t])
|
||||
|
||||
return (
|
||||
<div className="doc-panel">
|
||||
<div className="history-header toolbar-container">
|
||||
<Toolbar diff={diff} selection={selection} />
|
||||
</div>
|
||||
<div className="doc-container">
|
||||
<Main diff={diff} isLoading={isLoading || loadingFileDiffs} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DiffView
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
EditorSelection,
|
||||
EditorState,
|
||||
Extension,
|
||||
StateEffect,
|
||||
} from '@codemirror/state'
|
||||
import { EditorView, lineNumbers } from '@codemirror/view'
|
||||
import { indentationMarkers } from '@replit/codemirror-indentation-markers'
|
||||
import { highlights, setHighlightsEffect } from '../../extensions/highlights'
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
import { theme, Options, setOptionsTheme } from '../../extensions/theme'
|
||||
import { indentUnit } from '@codemirror/language'
|
||||
import { Highlight } from '../../services/types/doc'
|
||||
import useIsMounted from '../../../../shared/hooks/use-is-mounted'
|
||||
import {
|
||||
highlightLocations,
|
||||
highlightLocationsField,
|
||||
scrollToHighlight,
|
||||
} from '../../extensions/highlight-locations'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { inlineBackground } from '../../../source-editor/extensions/inline-background'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
function extensions(themeOptions: Options): Extension[] {
|
||||
return [
|
||||
EditorView.editable.of(false),
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.contentAttributes.of({ tabindex: '0' }),
|
||||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
indentUnit.of(' '), // TODO: Vary this by file type
|
||||
indentationMarkers({ hideFirstIndent: true, highlightActiveBlock: false }),
|
||||
highlights(),
|
||||
highlightLocations(),
|
||||
theme(themeOptions),
|
||||
inlineBackground(false),
|
||||
]
|
||||
}
|
||||
|
||||
function DocumentDiffViewer({
|
||||
doc,
|
||||
highlights,
|
||||
}: {
|
||||
doc: string
|
||||
highlights: Highlight[]
|
||||
}) {
|
||||
const { userSettings } = useUserSettingsContext()
|
||||
const { fontFamily, fontSize, lineHeight } = userSettings
|
||||
const isMounted = useIsMounted()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [state, setState] = useState(() => {
|
||||
return EditorState.create({
|
||||
doc,
|
||||
extensions: extensions({
|
||||
fontSize,
|
||||
fontFamily,
|
||||
lineHeight,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const [view] = useState<EditorView>(() => {
|
||||
return new EditorView({
|
||||
state,
|
||||
dispatch: tr => {
|
||||
view.update([tr])
|
||||
if (isMounted.current) {
|
||||
setState(view.state)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const highlightLocations = state.field(highlightLocationsField)
|
||||
|
||||
// Append the editor view DOM to the container node when mounted
|
||||
const containerRef = useCallback(
|
||||
node => {
|
||||
if (node) {
|
||||
node.appendChild(view.dom)
|
||||
}
|
||||
},
|
||||
[view]
|
||||
)
|
||||
|
||||
const scrollToPrevious = useCallback(() => {
|
||||
if (highlightLocations.previous) {
|
||||
scrollToHighlight(view, highlightLocations.previous)
|
||||
}
|
||||
}, [highlightLocations.previous, view])
|
||||
|
||||
const scrollToNext = useCallback(() => {
|
||||
if (highlightLocations.next) {
|
||||
scrollToHighlight(view, highlightLocations.next)
|
||||
}
|
||||
}, [highlightLocations.next, view])
|
||||
|
||||
const { before, after } = highlightLocations
|
||||
|
||||
useEffect(() => {
|
||||
const effects: StateEffect<unknown>[] = [setHighlightsEffect.of(highlights)]
|
||||
if (highlights.length > 0) {
|
||||
const { from, to } = highlights[0].range
|
||||
effects.push(
|
||||
EditorView.scrollIntoView(EditorSelection.range(from, to), {
|
||||
y: 'center',
|
||||
})
|
||||
)
|
||||
}
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: doc },
|
||||
effects,
|
||||
})
|
||||
}, [doc, highlights, view])
|
||||
|
||||
// Update the document diff viewer theme whenever the font size, font family
|
||||
// or line height user setting changes
|
||||
useEffect(() => {
|
||||
view.dispatch(
|
||||
setOptionsTheme({
|
||||
fontSize,
|
||||
fontFamily,
|
||||
lineHeight,
|
||||
})
|
||||
)
|
||||
}, [view, fontSize, fontFamily, lineHeight])
|
||||
|
||||
return (
|
||||
<div className="document-diff-container">
|
||||
<div ref={containerRef} className="cm-viewer-container" />
|
||||
{before > 0 ? (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
leadingIcon="arrow_upward"
|
||||
onClick={scrollToPrevious}
|
||||
className="previous-highlight-button"
|
||||
>
|
||||
{t('n_more_updates_above', { count: before })}
|
||||
</OLButton>
|
||||
) : null}
|
||||
{after > 0 ? (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
leadingIcon="arrow_downward"
|
||||
onClick={scrollToNext}
|
||||
className="next-highlight-button"
|
||||
>
|
||||
{t('n_more_updates_below', { count: after })}
|
||||
</OLButton>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentDiffViewer
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Nullable } from '../../../../../../types/utils'
|
||||
import { Diff } from '../../services/types/doc'
|
||||
import DocumentDiffViewer from './document-diff-viewer'
|
||||
import LoadingSpinner from '../../../../shared/components/loading-spinner'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
|
||||
type MainProps = {
|
||||
diff: Nullable<Diff>
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
function Main({ diff, isLoading }: MainProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
if (!diff) {
|
||||
return <div className="history-content">No document</div>
|
||||
}
|
||||
|
||||
if (diff.binary) {
|
||||
return (
|
||||
<div className="history-content">
|
||||
<OLNotification content={t('binary_history_error')} type="info" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (diff.docDiff) {
|
||||
const { doc, highlights } = diff.docDiff
|
||||
return <DocumentDiffViewer doc={doc} highlights={highlights} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default Main
|
||||
@@ -0,0 +1,47 @@
|
||||
import { formatTime } from '@/features/utils/format-date'
|
||||
import { useMemo } from 'react'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type RestoreFileConfirmModalProps = {
|
||||
show: boolean
|
||||
timestamp: number
|
||||
onConfirm: () => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
export function RestoreFileConfirmModal({
|
||||
show,
|
||||
timestamp,
|
||||
onConfirm,
|
||||
onHide,
|
||||
}: RestoreFileConfirmModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const date = useMemo(() => formatTime(timestamp, 'Do MMMM'), [timestamp])
|
||||
const time = useMemo(() => formatTime(timestamp, 'h:mm a'), [timestamp])
|
||||
|
||||
return (
|
||||
<OLModal show={show} onHide={onHide}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('restore_file_confirmation_title')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
{t('restore_file_confirmation_message', { date, time })}
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={onHide}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton variant="primary" onClick={onConfirm}>
|
||||
{t('restore')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function RestoreFileErrorModal({
|
||||
resetErrorBoundary,
|
||||
}: {
|
||||
resetErrorBoundary: VoidFunction
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLModal show onHide={resetErrorBoundary}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('restore_file_error_title')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>{t('restore_file_error_message')}</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={resetErrorBoundary}>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function RestoreProjectErrorModal({
|
||||
resetErrorBoundary,
|
||||
}: {
|
||||
resetErrorBoundary: VoidFunction
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLModal show onHide={resetErrorBoundary}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>
|
||||
{t('an_error_occured_while_restoring_project')}
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
{t(
|
||||
'there_was_a_problem_restoring_the_project_please_try_again_in_a_few_moments_or_contact_us'
|
||||
)}
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={resetErrorBoundary}>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import { formatDate } from '@/utils/dates'
|
||||
import { useCallback } from 'react'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type RestoreProjectModalProps = {
|
||||
setShow: React.Dispatch<React.SetStateAction<boolean>>
|
||||
show: boolean
|
||||
isRestoring: boolean
|
||||
endTimestamp: number
|
||||
onRestore: () => void
|
||||
}
|
||||
|
||||
export const RestoreProjectModal = ({
|
||||
setShow,
|
||||
show,
|
||||
endTimestamp,
|
||||
isRestoring,
|
||||
onRestore,
|
||||
}: RestoreProjectModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setShow(false)
|
||||
}, [setShow])
|
||||
|
||||
return (
|
||||
<OLModal onHide={() => setShow(false)} show={show}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('restore_this_version')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
<p>
|
||||
{t('your_current_project_will_revert_to_the_version_from_time', {
|
||||
timestamp: formatDate(endTimestamp),
|
||||
})}
|
||||
</p>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={onCancel} disabled={isRestoring}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={onRestore}
|
||||
disabled={isRestoring}
|
||||
isLoading={isRestoring}
|
||||
>
|
||||
{t('restore')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Trans } from 'react-i18next'
|
||||
import { formatTime } from '../../../../utils/format-date'
|
||||
import type { HistoryContextValue } from '../../../context/types/history-context-value'
|
||||
|
||||
type ToolbarDatetimeProps = {
|
||||
selection: HistoryContextValue['selection']
|
||||
}
|
||||
|
||||
export default function ToolbarDatetime({ selection }: ToolbarDatetimeProps) {
|
||||
return (
|
||||
<div className="history-react-toolbar-datetime">
|
||||
{selection.comparing ? (
|
||||
<Trans
|
||||
i18nKey="comparing_from_x_to_y"
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
components={[<time className="history-react-toolbar-time" />]}
|
||||
values={{
|
||||
startTime: formatTime(
|
||||
selection.updateRange?.fromVTimestamp,
|
||||
'Do MMMM · h:mm a'
|
||||
),
|
||||
endTime: formatTime(
|
||||
selection.updateRange?.toVTimestamp,
|
||||
'Do MMMM · h:mm a'
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="viewing_x"
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
components={[<time className="history-react-toolbar-time" />]}
|
||||
values={{
|
||||
endTime: formatTime(
|
||||
selection.updateRange?.toVTimestamp,
|
||||
'Do MMMM · h:mm a'
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { HistoryContextValue } from '../../../context/types/history-context-value'
|
||||
import type { Diff } from '../../../services/types/doc'
|
||||
import type { Nullable } from '../../../../../../../types/utils'
|
||||
|
||||
type ToolbarFileInfoProps = {
|
||||
diff: Nullable<Diff>
|
||||
selection: HistoryContextValue['selection']
|
||||
}
|
||||
|
||||
export default function ToolbarFileInfo({
|
||||
diff,
|
||||
selection,
|
||||
}: ToolbarFileInfoProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="history-react-toolbar-file-info">
|
||||
{t('x_changes_in', {
|
||||
count: diff?.docDiff?.highlights?.length ?? 0,
|
||||
})}
|
||||
|
||||
<strong>{getFileName(selection)}</strong>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getFileName(selection: HistoryContextValue['selection']) {
|
||||
const filePathParts = selection?.selectedFile?.pathname?.split('/')
|
||||
let fileName
|
||||
if (filePathParts) {
|
||||
fileName = filePathParts[filePathParts.length - 1]
|
||||
}
|
||||
|
||||
return fileName
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRestoreDeletedFile } from '../../../context/hooks/use-restore-deleted-file'
|
||||
import type { HistoryContextValue } from '../../../context/types/history-context-value'
|
||||
|
||||
type ToolbarRestoreFileButtonProps = {
|
||||
selection: HistoryContextValue['selection']
|
||||
}
|
||||
|
||||
export default function ToolbarRestoreFileButton({
|
||||
selection,
|
||||
}: ToolbarRestoreFileButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { restoreDeletedFile, isLoading } = useRestoreDeletedFile()
|
||||
|
||||
return (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="history-react-toolbar-restore-file-button"
|
||||
isLoading={isLoading}
|
||||
onClick={() => restoreDeletedFile(selection)}
|
||||
>
|
||||
{t('restore_file')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { HistoryContextValue } from '../../../context/types/history-context-value'
|
||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||
import { RestoreFileConfirmModal } from '../modals/restore-file-confirm-modal'
|
||||
import { useState } from 'react'
|
||||
import { RestoreFileErrorModal } from '../modals/restore-file-error-modal'
|
||||
import { useRestoreSelectedFile } from '@/features/history/context/hooks/use-restore-selected-file'
|
||||
|
||||
type ToolbarRevertingFileButtonProps = {
|
||||
selection: HistoryContextValue['selection']
|
||||
}
|
||||
|
||||
function ToolbarRestoreFileToVersionButton({
|
||||
selection,
|
||||
}: ToolbarRevertingFileButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const { restoreSelectedFile, isLoading } = useRestoreSelectedFile()
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
|
||||
if (!selection.updateRange || !selection.selectedFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RestoreFileConfirmModal
|
||||
show={showConfirmModal}
|
||||
timestamp={selection.updateRange.toVTimestamp}
|
||||
onConfirm={() => {
|
||||
setShowConfirmModal(false)
|
||||
restoreSelectedFile(selection)
|
||||
}}
|
||||
onHide={() => setShowConfirmModal(false)}
|
||||
/>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
isLoading={isLoading}
|
||||
onClick={() => setShowConfirmModal(true)}
|
||||
>
|
||||
{t('restore_file_version')}
|
||||
</OLButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(
|
||||
ToolbarRestoreFileToVersionButton,
|
||||
RestoreFileErrorModal
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Nullable } from '../../../../../../../types/utils'
|
||||
import type { Diff } from '../../../services/types/doc'
|
||||
import type { HistoryContextValue } from '../../../context/types/history-context-value'
|
||||
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 ToolbarRestoreFileToVersionButton from './toolbar-restore-file-to-version-button'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import SplitTestBadge from '@/shared/components/split-test-badge'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
|
||||
type ToolbarProps = {
|
||||
diff: Nullable<Diff>
|
||||
selection: HistoryContextValue['selection']
|
||||
}
|
||||
|
||||
export default function Toolbar({ diff, selection }: ToolbarProps) {
|
||||
const { write } = usePermissionsContext()
|
||||
const hasRestoreFileToVersion = useFeatureFlag('revert-file')
|
||||
|
||||
const showRestoreFileToVersionButton =
|
||||
hasRestoreFileToVersion && selection.selectedFile && write
|
||||
|
||||
const showRestoreFileButton =
|
||||
selection.selectedFile &&
|
||||
isFileRemoved(selection.selectedFile) &&
|
||||
!showRestoreFileToVersionButton &&
|
||||
write
|
||||
|
||||
return (
|
||||
<div className="history-react-toolbar">
|
||||
<ToolbarDatetime selection={selection} />
|
||||
{selection.selectedFile?.pathname ? (
|
||||
<ToolbarFileInfo diff={diff} selection={selection} />
|
||||
) : null}
|
||||
{showRestoreFileButton ? (
|
||||
<ToolbarRestoreFileButton selection={selection} />
|
||||
) : null}
|
||||
{showRestoreFileToVersionButton ? (
|
||||
<>
|
||||
<ToolbarRestoreFileToVersionButton selection={selection} />
|
||||
<SplitTestBadge
|
||||
splitTestName="revert-file"
|
||||
displayOnVariants={['enabled']}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user