From 0648b8aa6c38bbf75dfc7f6bbd71113f5e653600 Mon Sep 17 00:00:00 2001 From: M Fahru Date: Thu, 27 Apr 2023 11:45:13 -0700 Subject: [PATCH] Implement deleted file restore for history react (#12753) * Add strikethrough to deleted file tree item in history file tree * Add "restore file" button on toolbar if the selected file have a `removed` operation. * Implement "restore file" functionality on removed file: - Refactor the `Selection` object in history context value since we need the `deletedAtV` data which currently is not passed through the state. - Refactor and clean up file tree type system to support passing through the whole `FileDiff` object for getting the `deletedAtV` data which only appear on `removed` operation - Implement `postJSON` with file restoration API and pass it on restore file onClick handler at toolbar * Implement loading behaviour while restoring file is inflight: - Add `loadingRestoreFile` to `LoadingState` - Change restore file button to `Restoring...` when in loading state. * Refactor: - Rename `DiffOperation` to `FileOperation` - Extract `isFileRemoved` and `isFileRenamed` to its own file - Extract `Toolbar` components into small files GitOrigin-RevId: 2e32ebd2165f73fc6533ff282a9c084162efd682 --- .../web/frontend/extracted-translations.json | 2 + .../dropdown/menu-item/compare.tsx | 1 - .../change-list/history-version-details.tsx | 1 - .../components/diff-view/diff-view.tsx | 42 ++++++----- .../history/components/diff-view/toolbar.tsx | 73 ------------------- .../diff-view/toolbar/toolbar-datetime.tsx | 43 +++++++++++ .../diff-view/toolbar/toolbar-file-info.tsx | 36 +++++++++ .../toolbar/toolbar-restore-file-button.tsx | 32 ++++++++ .../components/diff-view/toolbar/toolbar.tsx | 29 ++++++++ .../file-tree/history-file-tree-doc.tsx | 14 ++-- .../history-file-tree-folder-list.tsx | 7 +- .../file-tree/history-file-tree-item.tsx | 13 +++- .../history/context/history-context.tsx | 16 ++-- .../hooks/use-file-tree-item-selection.tsx | 11 +-- .../context/hooks/use-restore-deleted-file.ts | 44 +++++++++++ .../context/types/history-context-value.ts | 11 ++- .../js/features/history/services/api.ts | 12 ++- .../history/services/types/diff-operation.ts | 1 - .../history/services/types/file-operation.ts | 1 + .../features/history/services/types/file.ts | 10 +-- .../history/services/types/restore-file.ts | 4 + .../history/services/types/selection.ts | 2 +- .../history/utils/auto-select-file.ts | 10 +-- .../js/features/history/utils/file-diff.ts | 9 +++ .../js/features/history/utils/file-tree.ts | 63 ++++++++-------- .../stylesheets/app/editor/history-react.less | 15 +++- services/web/locales/en.json | 1 + .../history/components/toolbar.spec.tsx | 38 +++++++--- .../history/utils/auto-select-file.test.ts | 35 +++++++-- 29 files changed, 394 insertions(+), 182 deletions(-) delete mode 100644 services/web/frontend/js/features/history/components/diff-view/toolbar.tsx create mode 100644 services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-datetime.tsx create mode 100644 services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-file-info.tsx create mode 100644 services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-restore-file-button.tsx create mode 100644 services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar.tsx create mode 100644 services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts delete mode 100644 services/web/frontend/js/features/history/services/types/diff-operation.ts create mode 100644 services/web/frontend/js/features/history/services/types/file-operation.ts create mode 100644 services/web/frontend/js/features/history/services/types/restore-file.ts create mode 100644 services/web/frontend/js/features/history/utils/file-diff.ts diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 215529a616..8c93d082a9 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -715,6 +715,8 @@ "resend": "", "resend_confirmation_email": "", "resending_confirmation_email": "", + "restore_file": "", + "restoring": "", "reverse_x_sort_order": "", "revert_pending_plan_change": "", "review": "", diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/compare.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/compare.tsx index 4f341a9e07..332a0d4062 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/compare.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/compare.tsx @@ -44,7 +44,6 @@ function Compare({ }, comparing: true, files: [], - pathname: null, }) } diff --git a/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx b/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx index fef76114ae..d73276f296 100644 --- a/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx +++ b/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx @@ -24,7 +24,6 @@ function HistoryVersionDetails({ updateRange: { fromV, toV, fromVTimestamp, toVTimestamp }, comparing: false, files: [], - pathname: null, }) } } diff --git a/services/web/frontend/js/features/history/components/diff-view/diff-view.tsx b/services/web/frontend/js/features/history/components/diff-view/diff-view.tsx index 58b74e2c87..3a07c2fc93 100644 --- a/services/web/frontend/js/features/history/components/diff-view/diff-view.tsx +++ b/services/web/frontend/js/features/history/components/diff-view/diff-view.tsx @@ -1,7 +1,7 @@ -import Toolbar from './toolbar' +import { useEffect, useState } from 'react' +import Toolbar from './toolbar/toolbar' import Main from './main' import { Diff, DocDiffResponse } from '../../services/types/doc' -import { useEffect, useState } from 'react' import { Nullable } from '../../../../../../types/utils' import { useHistoryContext } from '../../context/history-context' import { diffDoc } from '../../services/api' @@ -14,35 +14,37 @@ function DiffView() { const { isLoading, runAsync } = useAsync() - const { updateRange, pathname } = selection + const { updateRange, selectedFile } = selection useEffect(() => { - if (!updateRange || !pathname) { + if (!updateRange || !selectedFile?.pathname) { return } const { fromV, toV } = updateRange // TODO: Error handling - runAsync(diffDoc(projectId, fromV, toV, pathname)).then(data => { - let diff: Diff | undefined + runAsync(diffDoc(projectId, fromV, toV, selectedFile.pathname)).then( + data => { + let diff: Diff | undefined - if (!data?.diff) { - setDiff(null) - } - - if ('binary' in data.diff) { - diff = { binary: true } - } else { - diff = { - binary: false, - docDiff: highlightsFromDiffResponse(data.diff), + if (!data?.diff) { + setDiff(null) } - } - setDiff(diff) - }) - }, [projectId, runAsync, updateRange, pathname]) + if ('binary' in data.diff) { + diff = { binary: true } + } else { + diff = { + binary: false, + docDiff: highlightsFromDiffResponse(data.diff), + } + } + + setDiff(diff) + } + ) + }, [projectId, runAsync, updateRange, selectedFile]) return (
diff --git a/services/web/frontend/js/features/history/components/diff-view/toolbar.tsx b/services/web/frontend/js/features/history/components/diff-view/toolbar.tsx deleted file mode 100644 index b3393d2c28..0000000000 --- a/services/web/frontend/js/features/history/components/diff-view/toolbar.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Trans, useTranslation } from 'react-i18next' -import { formatTime } from '../../../utils/format-date' -import type { Nullable } from '../../../../../../types/utils' -import type { Diff } from '../../services/types/doc' -import type { HistoryContextValue } from '../../context/types/history-context-value' - -type ToolbarProps = { - diff: Nullable - selection: HistoryContextValue['selection'] -} - -function Toolbar({ diff, selection }: ToolbarProps) { - const { t } = useTranslation() - - if (!selection) return null - - return ( -
-
- {selection.comparing ? ( - ]} - values={{ - startTime: formatTime( - selection.updateRange?.fromVTimestamp, - 'Do MMMM · h:mm a' - ), - endTime: formatTime( - selection.updateRange?.toVTimestamp, - 'Do MMMM · h:mm a' - ), - }} - /> - ) : ( - ]} - values={{ - endTime: formatTime( - selection.updateRange?.toVTimestamp, - 'Do MMMM · h:mm a' - ), - }} - /> - )} -
- {selection.pathname ? ( -
- {t('x_changes_in', { - count: diff?.docDiff?.highlights.length ?? 0, - })} -   - {getFileName(selection)} -
- ) : null} -
- ) -} - -function getFileName(selection: HistoryContextValue['selection']) { - const filePathParts = selection?.pathname?.split('/') - let fileName - if (filePathParts) { - fileName = filePathParts[filePathParts.length - 1] - } - - return fileName -} - -export default Toolbar diff --git a/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-datetime.tsx b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-datetime.tsx new file mode 100644 index 0000000000..020881cfd2 --- /dev/null +++ b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-datetime.tsx @@ -0,0 +1,43 @@ +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 ( +
+ {selection.comparing ? ( + ]} + values={{ + startTime: formatTime( + selection.updateRange?.fromVTimestamp, + 'Do MMMM · h:mm a' + ), + endTime: formatTime( + selection.updateRange?.toVTimestamp, + 'Do MMMM · h:mm a' + ), + }} + /> + ) : ( + ]} + values={{ + endTime: formatTime( + selection.updateRange?.toVTimestamp, + 'Do MMMM · h:mm a' + ), + }} + /> + )} +
+ ) +} diff --git a/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-file-info.tsx b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-file-info.tsx new file mode 100644 index 0000000000..3171dcbe81 --- /dev/null +++ b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-file-info.tsx @@ -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 + selection: HistoryContextValue['selection'] +} + +export default function ToolbarFileInfo({ + diff, + selection, +}: ToolbarFileInfoProps) { + const { t } = useTranslation() + + return ( +
+ {t('x_changes_in', { + count: diff?.docDiff?.highlights?.length ?? 0, + })} +   + {getFileName(selection)} +
+ ) +} + +function getFileName(selection: HistoryContextValue['selection']) { + const filePathParts = selection?.selectedFile?.pathname?.split('/') + let fileName + if (filePathParts) { + fileName = filePathParts[filePathParts.length - 1] + } + + return fileName +} diff --git a/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-restore-file-button.tsx b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-restore-file-button.tsx new file mode 100644 index 0000000000..ec51e5f340 --- /dev/null +++ b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar-restore-file-button.tsx @@ -0,0 +1,32 @@ +import { Button } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import { useHistoryContext } from '../../../context/history-context' +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 { loadingState } = useHistoryContext() + + const onRestoreFile = useRestoreDeletedFile() + + return ( + + ) +} 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 new file mode 100644 index 0000000000..9f75b8f449 --- /dev/null +++ b/services/web/frontend/js/features/history/components/diff-view/toolbar/toolbar.tsx @@ -0,0 +1,29 @@ +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' + +type ToolbarProps = { + diff: Nullable + selection: HistoryContextValue['selection'] +} + +export default function Toolbar({ diff, selection }: ToolbarProps) { + const showRestoreFileButton = + selection.selectedFile && isFileRemoved(selection.selectedFile) + + return ( +
+ + {selection.selectedFile?.pathname ? ( + + ) : null} + {showRestoreFileButton ? ( + + ) : null} +
+ ) +} diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx index c2e8404a68..a6fe5851d4 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx @@ -1,22 +1,20 @@ +import classNames from 'classnames' import HistoryFileTreeItem from './history-file-tree-item' import iconTypeFromName from '../../../file-tree/util/icon-type-from-name' import Icon from '../../../../shared/components/icon' import { useFileTreeItemSelection } from '../../context/hooks/use-file-tree-item-selection' -import { DiffOperation } from '../../services/types/diff-operation' -import classNames from 'classnames' +import type { FileDiff } from '../../services/types/file' type HistoryFileTreeDocProps = { + file: FileDiff name: string - pathname: string - operation?: DiffOperation } export default function HistoryFileTreeDoc({ + file, name, - pathname, - operation, }: HistoryFileTreeDocProps) { - const { isSelected, onClick } = useFileTreeItemSelection(pathname) + const { isSelected, onClick } = useFileTreeItemSelection(file) return (
  • { return ( - + ) })} {children} diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx index b8d9509e23..dbcd8d137d 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx @@ -1,10 +1,11 @@ +import classNames from 'classnames' import type { ReactNode } from 'react' -import type { DiffOperation } from '../../services/types/diff-operation' +import type { FileOperation } from '../../services/types/file-operation' import Badge from '../../../../shared/components/badge' type FileTreeItemProps = { name: string - operation?: DiffOperation + operation?: FileOperation icons: ReactNode } @@ -17,7 +18,13 @@ export default function HistoryFileTreeItem({
    {icons}