mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
feat(frontend): delete revision
Signed-off-by: Avinash <avinash.kumar.cs92@gmail.com>
This commit is contained in:
parent
354a51fd72
commit
a948493410
8 changed files with 296 additions and 48 deletions
96
frontend/cypress/e2e/revision.spec.ts
Normal file
96
frontend/cypress/e2e/revision.spec.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { testNoteId } from '../support/visit-test-editor'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
describe('Revision modal', () => {
|
||||||
|
const testTitle = 'testContent'
|
||||||
|
const testContent = `---\ntitle: ${testTitle}\n---\nThis is some test content`
|
||||||
|
|
||||||
|
const defaultCreatedAt = '2021-12-29T17:54:11.000Z'
|
||||||
|
const formattedDate = DateTime.fromISO(defaultCreatedAt).toFormat('DDDD T')
|
||||||
|
const revisionPayload = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
createdAt: defaultCreatedAt,
|
||||||
|
length: 2788,
|
||||||
|
authorUsernames: [],
|
||||||
|
anonymousAuthorCount: 4,
|
||||||
|
title: 'Features',
|
||||||
|
description: 'Many features, such wow!',
|
||||||
|
tags: ['hedgedoc', 'demo', 'react']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
createdAt: defaultCreatedAt,
|
||||||
|
length: 2782,
|
||||||
|
authorUsernames: [],
|
||||||
|
anonymousAuthorCount: 2,
|
||||||
|
title: 'Features',
|
||||||
|
description: 'Many more features, such wow!',
|
||||||
|
tags: ['hedgedoc', 'demo', 'react']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.intercept('GET', `api/private/notes/${testNoteId}/revisions`, revisionPayload)
|
||||||
|
cy.visitTestNote()
|
||||||
|
cy.getByCypressId('sidebar.revision.button').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can delete revisions', () => {
|
||||||
|
cy.intercept('DELETE', `api/private/notes/${testNoteId}/revisions`, {
|
||||||
|
statusCode: 204
|
||||||
|
})
|
||||||
|
const cardsContents = [formattedDate, 'Length: 2788', 'Anonymous authors or guests: 4']
|
||||||
|
|
||||||
|
cardsContents.forEach((content) => cy.getByCypressId('revision.modal.lists').contains(content))
|
||||||
|
|
||||||
|
cy.getByCypressId('revision.modal.revert.button').should('be.disabled')
|
||||||
|
cy.getByCypressId('revision.modal.download.button').should('be.disabled')
|
||||||
|
cy.getByCypressId('revision.modal.delete.button').click()
|
||||||
|
cy.getByCypressId('revision.delete.modal').should('be.visible')
|
||||||
|
|
||||||
|
cy.getByCypressId('revision.delete.button').click()
|
||||||
|
cy.getByCypressId('revision.delete.modal').should('not.exist')
|
||||||
|
cy.getByCypressId('sidebar.revision.modal').should('be.visible')
|
||||||
|
})
|
||||||
|
it('can handle fail of revision deletion', () => {
|
||||||
|
cy.intercept('DELETE', `api/private/notes/${testNoteId}/revisions`, {
|
||||||
|
statusCode: 400
|
||||||
|
})
|
||||||
|
cy.getByCypressId('revision.modal.delete.button').click()
|
||||||
|
cy.getByCypressId('revision.delete.modal').should('be.visible')
|
||||||
|
|
||||||
|
cy.getByCypressId('revision.delete.button').click()
|
||||||
|
cy.getByCypressId('revision.delete.modal').should('not.exist')
|
||||||
|
cy.getByCypressId('notification-toast').should('be.visible')
|
||||||
|
cy.getByCypressId('sidebar.revision.modal').should('be.visible')
|
||||||
|
})
|
||||||
|
it('can download revisions', () => {
|
||||||
|
cy.intercept('GET', '/api/private/notes/mock-note/revisions/1', {
|
||||||
|
id: 1,
|
||||||
|
createdAt: defaultCreatedAt,
|
||||||
|
title: 'Features',
|
||||||
|
description: 'Many more features, such wow!',
|
||||||
|
tags: ['hedgedoc', 'demo', 'react'],
|
||||||
|
patch: testContent,
|
||||||
|
edits: [],
|
||||||
|
length: 2788,
|
||||||
|
authorUsernames: [],
|
||||||
|
anonymousAuthorCount: 4,
|
||||||
|
content: testContent
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadFolder = Cypress.config('downloadsFolder')
|
||||||
|
const fileName = `mock-note-${defaultCreatedAt.replace(/:/g, '_')}.md`
|
||||||
|
const filePath = join(downloadFolder, fileName)
|
||||||
|
|
||||||
|
cy.getByCypressId('revision.modal.lists').contains(formattedDate).click()
|
||||||
|
cy.getByCypressId('revision.modal.download.button').click()
|
||||||
|
cy.readFile(filePath).should('contain', testContent)
|
||||||
|
})
|
||||||
|
})
|
|
@ -381,6 +381,13 @@
|
||||||
"download": "Download selected revision",
|
"download": "Download selected revision",
|
||||||
"guestCount": "Anonymous authors or guests"
|
"guestCount": "Anonymous authors or guests"
|
||||||
},
|
},
|
||||||
|
"deleteRevision": {
|
||||||
|
"title": "Delete Revisions for current note",
|
||||||
|
"question": "Do you really want to remove all the previous revisions except the current of this note?",
|
||||||
|
"warning": "All the previous revisions except the current will be deleted for this note. This process is irreversible.",
|
||||||
|
"error": "An error occurred while deleting revisions of this note.",
|
||||||
|
"button": "Delete Revisions"
|
||||||
|
},
|
||||||
"clipboardImport": {
|
"clipboardImport": {
|
||||||
"title": "Import from clipboard",
|
"title": "Import from clipboard",
|
||||||
"insertMarkdown": "Paste your markdown or webpage here…"
|
"insertMarkdown": "Paste your markdown or webpage here…"
|
||||||
|
|
|
@ -4,10 +4,12 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useBooleanState } from '../../../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../../../hooks/common/use-boolean-state'
|
||||||
|
import { cypressId } from '../../../../../utils/cypress-attribute'
|
||||||
import { SidebarButton } from '../../sidebar-button/sidebar-button'
|
import { SidebarButton } from '../../sidebar-button/sidebar-button'
|
||||||
import type { SpecificSidebarEntryProps } from '../../types'
|
import type { SpecificSidebarEntryProps } from '../../types'
|
||||||
|
import { RevisionDeleteModal } from './revisions-modal/delete-revision-modal'
|
||||||
import { RevisionModal } from './revisions-modal/revision-modal'
|
import { RevisionModal } from './revisions-modal/revision-modal'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment, useCallback } from 'react'
|
||||||
import { ClockHistory as IconClockHistory } from 'react-bootstrap-icons'
|
import { ClockHistory as IconClockHistory } from 'react-bootstrap-icons'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
|
@ -19,13 +21,25 @@ import { Trans } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const RevisionSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
export const RevisionSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
|
const [revisionsDeleteModalVisibility, showRevisionsDeleteModal, closeRevisionsDeleteModal] = useBooleanState()
|
||||||
|
|
||||||
|
const onDeleteRevisions = useCallback(() => {
|
||||||
|
closeRevisionsDeleteModal()
|
||||||
|
showModal()
|
||||||
|
}, [closeRevisionsDeleteModal, showModal])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SidebarButton hide={hide} className={className} icon={IconClockHistory} onClick={showModal}>
|
<SidebarButton
|
||||||
|
{...cypressId('sidebar.revision.button')}
|
||||||
|
hide={hide}
|
||||||
|
className={className}
|
||||||
|
icon={IconClockHistory}
|
||||||
|
onClick={showModal}>
|
||||||
<Trans i18nKey={'editor.modal.revision.title'} />
|
<Trans i18nKey={'editor.modal.revision.title'} />
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
<RevisionModal show={modalVisibility} onHide={closeModal} />
|
<RevisionModal show={modalVisibility} onHide={closeModal} onShowDeleteModal={showRevisionsDeleteModal} />
|
||||||
|
<RevisionDeleteModal show={revisionsDeleteModalVisibility} onHide={onDeleteRevisions} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { deleteRevisionsForNote } from '../../../../../../api/revisions'
|
||||||
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
|
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||||
|
import type { ModalVisibilityProps } from '../../../../../common/modals/common-modal'
|
||||||
|
import { CommonModal } from '../../../../../common/modals/common-modal'
|
||||||
|
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This modal for deletion of notes's revision
|
||||||
|
*
|
||||||
|
* @param show true to show the modal, false otherwise.
|
||||||
|
* @param onHide Callback that is fired when the modal is requested to close.
|
||||||
|
*/
|
||||||
|
export const RevisionDeleteModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||||
|
|
||||||
|
const deleteAllRevisions = useCallback(() => {
|
||||||
|
deleteRevisionsForNote(noteId).catch(showErrorNotification('editor.modal.deleteRevision.error')).finally(onHide)
|
||||||
|
}, [noteId, onHide, showErrorNotification])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonModal
|
||||||
|
show={show}
|
||||||
|
onHide={onHide}
|
||||||
|
titleI18nKey={'editor.modal.deleteRevision.title'}
|
||||||
|
showCloseButton={true}
|
||||||
|
{...cypressId('revision.delete.modal')}>
|
||||||
|
<Modal.Body>
|
||||||
|
<h6>
|
||||||
|
<Trans i18nKey={'editor.modal.deleteRevision.warning'} />
|
||||||
|
</h6>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant='danger' onClick={deleteAllRevisions} {...cypressId('revision.delete.button')}>
|
||||||
|
<Trans i18nKey={'editor.modal.deleteRevision.button'} />
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</CommonModal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,17 +3,19 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { getAllRevisions } from '../../../../../../api/revisions'
|
import type { RevisionMetadata } from '../../../../../../api/revisions/types'
|
||||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||||
import { AsyncLoadingBoundary } from '../../../../../common/async-loading-boundary/async-loading-boundary'
|
import { AsyncLoadingBoundary } from '../../../../../common/async-loading-boundary/async-loading-boundary'
|
||||||
import { RevisionListEntry } from './revision-list-entry'
|
import { RevisionListEntry } from './revision-list-entry'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { ListGroup } from 'react-bootstrap'
|
import { ListGroup } from 'react-bootstrap'
|
||||||
import { useAsync } from 'react-use'
|
|
||||||
|
|
||||||
export interface RevisionListProps {
|
interface RevisionListProps {
|
||||||
selectedRevisionId?: number
|
selectedRevisionId?: number
|
||||||
|
revisions?: RevisionMetadata[]
|
||||||
|
loadingRevisions: boolean
|
||||||
|
error?: Error | boolean
|
||||||
onRevisionSelect: (selectedRevisionId: number) => void
|
onRevisionSelect: (selectedRevisionId: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,20 +24,19 @@ export interface RevisionListProps {
|
||||||
*
|
*
|
||||||
* @param selectedRevisionId The currently selected revision
|
* @param selectedRevisionId The currently selected revision
|
||||||
* @param onRevisionSelect Callback that is executed when a list entry is selected
|
* @param onRevisionSelect Callback that is executed when a list entry is selected
|
||||||
|
* @param revisions List of all the revisions
|
||||||
|
* @param error Indicates an error occurred
|
||||||
|
* @param loadingRevisions Boolean for showing loading state
|
||||||
*/
|
*/
|
||||||
export const RevisionList: React.FC<RevisionListProps> = ({ selectedRevisionId, onRevisionSelect }) => {
|
export const RevisionList: React.FC<RevisionListProps> = ({
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
selectedRevisionId,
|
||||||
|
onRevisionSelect,
|
||||||
const {
|
revisions,
|
||||||
value: revisions,
|
loadingRevisions,
|
||||||
error,
|
error
|
||||||
loading
|
}) => {
|
||||||
} = useAsync(() => {
|
|
||||||
return getAllRevisions(noteIdentifier)
|
|
||||||
}, [noteIdentifier])
|
|
||||||
|
|
||||||
const revisionList = useMemo(() => {
|
const revisionList = useMemo(() => {
|
||||||
if (loading || !revisions) {
|
if (loadingRevisions || !revisions) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return revisions
|
return revisions
|
||||||
|
@ -52,11 +53,11 @@ export const RevisionList: React.FC<RevisionListProps> = ({ selectedRevisionId,
|
||||||
key={revisionListEntry.id}
|
key={revisionListEntry.id}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}, [loading, onRevisionSelect, revisions, selectedRevisionId])
|
}, [loadingRevisions, onRevisionSelect, revisions, selectedRevisionId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncLoadingBoundary loading={loading || !revisions} error={error} componentName={'revision list'}>
|
<AsyncLoadingBoundary loading={loadingRevisions || !revisions} error={error} componentName={'revision list'}>
|
||||||
<ListGroup>{revisionList}</ListGroup>
|
<ListGroup {...cypressId('revision.modal.lists')}>{revisionList}</ListGroup>
|
||||||
</AsyncLoadingBoundary>
|
</AsyncLoadingBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { getAllRevisions } from '../../../../../../api/revisions'
|
||||||
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
|
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
|
||||||
|
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||||
|
import { RevisionList } from './revision-list'
|
||||||
|
import type { RevisionModal } from './revision-modal'
|
||||||
|
import { RevisionModalFooter } from './revision-modal-footer'
|
||||||
|
import styles from './revision-modal.module.scss'
|
||||||
|
import { RevisionViewer } from './revision-viewer'
|
||||||
|
import React, { Fragment, useState } from 'react'
|
||||||
|
import { Col, Modal, Row } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useAsync } from 'react-use'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders list of revisions and all the actions buttons.
|
||||||
|
*
|
||||||
|
* @param onShowDeleteModal Callback to render revisions delete modal
|
||||||
|
* @param onHide Callback that gets triggered when revision modal is about to hide.
|
||||||
|
*/
|
||||||
|
export const RevisionModalBody = ({ onShowDeleteModal, onHide }: RevisionModal) => {
|
||||||
|
useTranslation()
|
||||||
|
const isOwner = useIsOwner()
|
||||||
|
const [selectedRevisionId, setSelectedRevisionId] = useState<number>()
|
||||||
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.id)
|
||||||
|
const { value: revisions, error, loading } = useAsync(() => getAllRevisions(noteIdentifier), [noteIdentifier])
|
||||||
|
|
||||||
|
const revisionLength = revisions?.length ?? 0
|
||||||
|
const enableDeleteRevisions = revisionLength > 1 && isOwner
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Modal.Body {...cypressId('sidebar.revision.modal')}>
|
||||||
|
<Row>
|
||||||
|
<Col lg={4} className={styles['scroll-col']}>
|
||||||
|
<RevisionList
|
||||||
|
error={error}
|
||||||
|
loadingRevisions={loading}
|
||||||
|
revisions={revisions}
|
||||||
|
onRevisionSelect={setSelectedRevisionId}
|
||||||
|
selectedRevisionId={selectedRevisionId}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col lg={8} className={styles['scroll-col']}>
|
||||||
|
<RevisionViewer selectedRevisionId={selectedRevisionId} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Modal.Body>
|
||||||
|
<RevisionModalFooter
|
||||||
|
selectedRevisionId={selectedRevisionId}
|
||||||
|
onHide={onHide}
|
||||||
|
disableDeleteRevisions={!enableDeleteRevisions}
|
||||||
|
onShowDeleteModal={onShowDeleteModal}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,27 +5,36 @@
|
||||||
*/
|
*/
|
||||||
import { getRevision } from '../../../../../../api/revisions'
|
import { getRevision } from '../../../../../../api/revisions'
|
||||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
|
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||||
import type { ModalVisibilityProps } from '../../../../../common/modals/common-modal'
|
import type { ModalVisibilityProps } from '../../../../../common/modals/common-modal'
|
||||||
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||||
|
import type { RevisionModalProps } from './revision-modal'
|
||||||
import { downloadRevision } from './utils'
|
import { downloadRevision } from './utils'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Button, Modal } from 'react-bootstrap'
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export interface RevisionModalFooterProps {
|
interface RevisionModalFooter {
|
||||||
selectedRevisionId?: number
|
selectedRevisionId?: number
|
||||||
|
disableDeleteRevisions: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RevisionModalFooterProps = RevisionModalProps & RevisionModalFooter & Pick<ModalVisibilityProps, 'onHide'>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the footer of the revision modal that includes buttons to download the currently selected revision or to
|
* Renders the footer of the revision modal that includes buttons to download the currently selected revision or to
|
||||||
* revert the note content back to that revision.
|
* revert the note content back to that revision.
|
||||||
*
|
*
|
||||||
* @param selectedRevisionId The currently selected revision id or undefined if no revision was selected.
|
* @param selectedRevisionId The currently selected revision id or undefined if no revision was selected.
|
||||||
* @param onHide Callback that is fired when the modal is about to be closed.
|
* @param onHide Callback that is fired when the modal is about to be closed.
|
||||||
|
* @param onShowDeleteModal Callback to render revision deleteModal.
|
||||||
|
* @param disableDeleteRevisions Boolean to disable delete button.
|
||||||
*/
|
*/
|
||||||
export const RevisionModalFooter: React.FC<RevisionModalFooterProps & Pick<ModalVisibilityProps, 'onHide'>> = ({
|
export const RevisionModalFooter: React.FC<RevisionModalFooterProps> = ({
|
||||||
selectedRevisionId,
|
selectedRevisionId,
|
||||||
onHide
|
onHide,
|
||||||
|
onShowDeleteModal,
|
||||||
|
disableDeleteRevisions
|
||||||
}) => {
|
}) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
|
@ -48,15 +57,35 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps & Pick<Modal
|
||||||
.catch(showErrorNotification(''))
|
.catch(showErrorNotification(''))
|
||||||
}, [noteIdentifier, selectedRevisionId, showErrorNotification])
|
}, [noteIdentifier, selectedRevisionId, showErrorNotification])
|
||||||
|
|
||||||
|
const openDeleteModal = useCallback(() => {
|
||||||
|
onHide?.()
|
||||||
|
onShowDeleteModal()
|
||||||
|
}, [onHide, onShowDeleteModal])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant='secondary' onClick={onHide}>
|
<Button variant='secondary' onClick={onHide}>
|
||||||
<Trans i18nKey={'common.close'} />
|
<Trans i18nKey={'common.close'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='danger' disabled={selectedRevisionId === undefined} onClick={onRevertToRevision}>
|
<Button
|
||||||
|
variant='danger'
|
||||||
|
onClick={openDeleteModal}
|
||||||
|
{...cypressId('revision.modal.delete.button')}
|
||||||
|
disabled={disableDeleteRevisions}>
|
||||||
|
<Trans i18nKey={'editor.modal.deleteRevision.button'} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='danger'
|
||||||
|
disabled={selectedRevisionId === undefined}
|
||||||
|
onClick={onRevertToRevision}
|
||||||
|
{...cypressId('revision.modal.revert.button')}>
|
||||||
<Trans i18nKey={'editor.modal.revision.revertButton'} />
|
<Trans i18nKey={'editor.modal.revision.revertButton'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='primary' disabled={selectedRevisionId === undefined} onClick={onDownloadRevision}>
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
disabled={selectedRevisionId === undefined}
|
||||||
|
onClick={onDownloadRevision}
|
||||||
|
{...cypressId('revision.modal.download.button')}>
|
||||||
<Trans i18nKey={'editor.modal.revision.download'} />
|
<Trans i18nKey={'editor.modal.revision.download'} />
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
|
|
|
@ -5,25 +5,26 @@
|
||||||
*/
|
*/
|
||||||
import type { ModalVisibilityProps } from '../../../../../common/modals/common-modal'
|
import type { ModalVisibilityProps } from '../../../../../common/modals/common-modal'
|
||||||
import { CommonModal } from '../../../../../common/modals/common-modal'
|
import { CommonModal } from '../../../../../common/modals/common-modal'
|
||||||
import { RevisionList } from './revision-list'
|
import { RevisionModalBody } from './revision-modal-body'
|
||||||
import { RevisionModalFooter } from './revision-modal-footer'
|
|
||||||
import styles from './revision-modal.module.scss'
|
import styles from './revision-modal.module.scss'
|
||||||
import { RevisionViewer } from './revision-viewer'
|
import React from 'react'
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { Col, Modal, Row } from 'react-bootstrap'
|
|
||||||
import { ClockHistory as IconClockHistory } from 'react-bootstrap-icons'
|
import { ClockHistory as IconClockHistory } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
export interface RevisionModalProps {
|
||||||
|
onShowDeleteModal: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RevisionModal = RevisionModalProps & ModalVisibilityProps
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal that shows the available revisions and allows for comparison between them.
|
* Modal that shows the available revisions and allows for comparison between them.
|
||||||
*
|
*
|
||||||
* @param show true to show the modal, false otherwise.
|
* @param show true to show the modal, false otherwise.
|
||||||
* @param onHide Callback that is fired when the modal is requested to close.
|
* @param onHide Callback that is fired when the modal is requested to close.
|
||||||
|
* @param onShowDeleteModal Callback to render revision delete modal.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export const RevisionModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
export const RevisionModal: React.FC<RevisionModal> = ({ show, onHide, onShowDeleteModal }) => {
|
||||||
useTranslation()
|
|
||||||
const [selectedRevisionId, setSelectedRevisionId] = useState<number>()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal
|
||||||
show={show}
|
show={show}
|
||||||
|
@ -33,17 +34,7 @@ export const RevisionModal: React.FC<ModalVisibilityProps> = ({ show, onHide })
|
||||||
showCloseButton={true}
|
showCloseButton={true}
|
||||||
modalSize={'xl'}
|
modalSize={'xl'}
|
||||||
additionalClasses={styles['revision-modal']}>
|
additionalClasses={styles['revision-modal']}>
|
||||||
<Modal.Body>
|
<RevisionModalBody show={show} onShowDeleteModal={onShowDeleteModal} onHide={onHide} />
|
||||||
<Row>
|
|
||||||
<Col lg={4} className={styles['scroll-col']}>
|
|
||||||
<RevisionList onRevisionSelect={setSelectedRevisionId} selectedRevisionId={selectedRevisionId} />
|
|
||||||
</Col>
|
|
||||||
<Col lg={8} className={styles['scroll-col']}>
|
|
||||||
<RevisionViewer selectedRevisionId={selectedRevisionId} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Modal.Body>
|
|
||||||
<RevisionModalFooter selectedRevisionId={selectedRevisionId} onHide={onHide} />
|
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue