mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 03:06:31 -05:00
Add note deletion and removal from history modals (#299)
* Fix history element's entry menu deletion button The note deletion button inside the EntryMenu of a history element has one button to remove the note from the user's history and one to delete the note from the system. Both buttons pointed to the history-removal. * Added modals for note deletion and note from history removal * Removed redundant code * Added CHANGELOG entry * Added note title in deletion/removal prompts * Refactored DeleteNoteItem and RemoveNoteEntryItem into one common component * Refactored DeleteRemoveNoteItem-component and added two composition components * Redesigned modal dialog to make the note title more clearly readable * Renamed the generic dropdown-with-deletion-modal-component
This commit is contained in:
parent
b23a73ac51
commit
e1e8a76fda
11 changed files with 136 additions and 43 deletions
|
@ -30,6 +30,7 @@
|
|||
- Users may now change their display name and password (only email accounts) on the new profile page
|
||||
- Highlighted code blocks can now use line wrapping and line numbers at once
|
||||
- Images, videos, and other non-text content is now wider in View Mode
|
||||
- Notes may now be deleted directly from the history page
|
||||
- CodiMD instances can now be branded either with a '@ <custom string>' or '@ <custom logo>' after the CodiMD logo and text
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -70,6 +70,12 @@
|
|||
"textWithFile": "While trying to import history from '{{fileName}}' an error occurred.",
|
||||
"textWithoutFile": "You did not provide any files to upload the history from.",
|
||||
"tooNewVersion": "The file '{{fileName}}' comes from a newer client and can't be imported."
|
||||
},
|
||||
"removeNote": {
|
||||
"title": "Remove note from history",
|
||||
"question": "Do you really want to remove this note from your history?",
|
||||
"warning": "This just removes the history entry and won't delete the note itself.",
|
||||
"button": "Remove note from history"
|
||||
}
|
||||
},
|
||||
"tableHeader": {
|
||||
|
@ -252,7 +258,8 @@
|
|||
"deleteNote": {
|
||||
"title": "Delete note",
|
||||
"question": "Do you really want to delete this note?",
|
||||
"warning": "All users will lose their connection."
|
||||
"warning": "All users will lose their connection. This process is irreversible.",
|
||||
"button": "Delete note"
|
||||
}
|
||||
},
|
||||
"embeddings": {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import { DropdownItemWithDeletionModal } from './dropdown-item-with-deletion-modal'
|
||||
|
||||
export interface DeleteNoteItemProps {
|
||||
onConfirm: () => void
|
||||
noteTitle: string
|
||||
}
|
||||
|
||||
export const DeleteNoteItem: React.FC<DeleteNoteItemProps> = ({ noteTitle, onConfirm }) => {
|
||||
return (
|
||||
<DropdownItemWithDeletionModal
|
||||
onConfirm={onConfirm}
|
||||
itemI18nKey={'landing.history.menu.deleteNote'}
|
||||
modalButtonI18nKey={'editor.modal.deleteNote.button'}
|
||||
modalIcon={'trash'}
|
||||
modalTitleI18nKey={'editor.modal.deleteNote.title'}
|
||||
modalQuestionI18nKey={'editor.modal.deleteNote.question'}
|
||||
modalWarningI18nKey={'editor.modal.deleteNote.warning'}
|
||||
noteTitle={noteTitle} />
|
||||
)
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import React, { Fragment, useState } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon, IconName } from '../../../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { DeletionModal } from '../../../../../common/modals/deletion-modal'
|
||||
|
||||
export interface DropdownItemWithDeletionModalProps {
|
||||
onConfirm: () => void
|
||||
itemI18nKey: string
|
||||
modalButtonI18nKey: string
|
||||
modalIcon: IconName
|
||||
modalTitleI18nKey: string
|
||||
modalQuestionI18nKey: string
|
||||
modalWarningI18nKey: string
|
||||
noteTitle: string
|
||||
}
|
||||
|
||||
export const DropdownItemWithDeletionModal: React.FC<DropdownItemWithDeletionModalProps> = ({
|
||||
onConfirm, noteTitle,
|
||||
modalTitleI18nKey, modalButtonI18nKey, itemI18nKey, modalIcon,
|
||||
modalQuestionI18nKey, modalWarningI18nKey
|
||||
}) => {
|
||||
useTranslation()
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Dropdown.Item onClick={() => setShowDialog(true)}>
|
||||
<ForkAwesomeIcon icon={modalIcon} fixedWidth={true} className="mx-2"/>
|
||||
<Trans i18nKey={itemI18nKey}/>
|
||||
</Dropdown.Item>
|
||||
<DeletionModal
|
||||
onConfirm={() => {
|
||||
setShowDialog(false)
|
||||
onConfirm()
|
||||
}}
|
||||
deletionButtonI18nKey={modalButtonI18nKey}
|
||||
show={showDialog}
|
||||
onHide={() => setShowDialog(false)}
|
||||
titleI18nKey={modalTitleI18nKey}>
|
||||
<h5><Trans i18nKey={modalQuestionI18nKey}/></h5>
|
||||
<ul>
|
||||
<li>{ noteTitle }</li>
|
||||
</ul>
|
||||
<h6><Trans i18nKey={modalWarningI18nKey}/></h6>
|
||||
</DeletionModal>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
import React from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../../../common/show-if/show-if'
|
||||
import { HistoryEntryOrigin } from '../history'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../../../../common/show-if/show-if'
|
||||
import { HistoryEntryOrigin } from '../../history'
|
||||
import './entry-menu.scss'
|
||||
import { DeleteNoteItem } from './delete-note-item'
|
||||
import { RemoveNoteEntryItem } from './remove-note-entry-item'
|
||||
|
||||
export interface EntryMenuProps {
|
||||
id: string;
|
||||
title: string
|
||||
location: HistoryEntryOrigin
|
||||
isDark: boolean;
|
||||
onRemove: () => void
|
||||
|
@ -15,7 +18,9 @@ export interface EntryMenuProps {
|
|||
className?: string
|
||||
}
|
||||
|
||||
const EntryMenu: React.FC<EntryMenuProps> = ({ id, location, isDark, onRemove, onDelete, className }) => {
|
||||
const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRemove, onDelete, className }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<Dropdown className={`d-inline-flex ${className || ''}`}>
|
||||
<Dropdown.Toggle variant={isDark ? 'secondary' : 'light'} id={`dropdown-card-${id}`} className='no-arrow history-menu d-inline-flex align-items-center'>
|
||||
|
@ -40,17 +45,11 @@ const EntryMenu: React.FC<EntryMenuProps> = ({ id, location, isDark, onRemove, o
|
|||
<Trans i18nKey="landing.history.menu.entryRemote"/>
|
||||
</Dropdown.Item>
|
||||
</ShowIf>
|
||||
<Dropdown.Item onClick={onRemove}>
|
||||
<ForkAwesomeIcon icon="archive" fixedWidth={true} className="mx-2"/>
|
||||
<Trans i18nKey="landing.history.menu.removeEntry"/>
|
||||
</Dropdown.Item>
|
||||
<RemoveNoteEntryItem onConfirm={onRemove} noteTitle={title} />
|
||||
|
||||
<Dropdown.Divider/>
|
||||
|
||||
<Dropdown.Item onClick={onDelete}>
|
||||
<ForkAwesomeIcon icon="trash" fixedWidth={true} className="mx-2"/>
|
||||
<Trans i18nKey="landing.history.menu.deleteNote"/>
|
||||
</Dropdown.Item>
|
||||
<DeleteNoteItem onConfirm={onDelete} noteTitle={title} />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import { DropdownItemWithDeletionModal } from './dropdown-item-with-deletion-modal'
|
||||
|
||||
export interface RemoveNoteEntryItemProps {
|
||||
onConfirm: () => void
|
||||
noteTitle: string
|
||||
}
|
||||
|
||||
export const RemoveNoteEntryItem: React.FC<RemoveNoteEntryItemProps> = ({ noteTitle, onConfirm }) => {
|
||||
return (
|
||||
<DropdownItemWithDeletionModal
|
||||
onConfirm={onConfirm}
|
||||
itemI18nKey={'landing.history.menu.removeEntry'}
|
||||
modalButtonI18nKey={'landing.history.modal.removeNote.button'}
|
||||
modalIcon={'archive'}
|
||||
modalTitleI18nKey={'landing.history.modal.removeNote.title'}
|
||||
modalQuestionI18nKey={'landing.history.modal.removeNote.question'}
|
||||
modalWarningI18nKey={'landing.history.modal.removeNote.warning'}
|
||||
noteTitle={noteTitle} />
|
||||
)
|
||||
}
|
|
@ -4,12 +4,12 @@ import { Badge, Card } from 'react-bootstrap'
|
|||
import { Link } from 'react-router-dom'
|
||||
import { formatHistoryDate } from '../../../../../utils/historyUtils'
|
||||
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { EntryMenu } from '../common/entry-menu'
|
||||
import { EntryMenu } from '../common/entry-menu/entry-menu'
|
||||
import { PinButton } from '../common/pin-button'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
import './history-card.scss'
|
||||
|
||||
export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick }) => {
|
||||
export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick, onDeleteClick }) => {
|
||||
return (
|
||||
<div className="p-2 col-xs-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<Card className="card-min-height" text={'dark'} bg={'light'}>
|
||||
|
@ -36,10 +36,11 @@ export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, on
|
|||
<div className={'d-flex flex-column'}>
|
||||
<EntryMenu
|
||||
id={entry.id}
|
||||
title={entry.title}
|
||||
location={entry.location}
|
||||
isDark={false}
|
||||
onRemove={() => onRemoveClick(entry.id, entry.location)}
|
||||
onDelete={() => onRemoveClick(entry.id, entry.location)}
|
||||
onDelete={() => onDeleteClick(entry.id, entry.location)}
|
||||
/>
|
||||
</div>
|
||||
</Card.Body>
|
||||
|
|
|
@ -2,11 +2,11 @@ import React from 'react'
|
|||
import { Badge } from 'react-bootstrap'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { formatHistoryDate } from '../../../../../utils/historyUtils'
|
||||
import { EntryMenu } from '../common/entry-menu'
|
||||
import { EntryMenu } from '../common/entry-menu/entry-menu'
|
||||
import { PinButton } from '../common/pin-button'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
|
||||
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick }) => {
|
||||
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick, onDeleteClick }) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
|
@ -25,10 +25,11 @@ export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick
|
|||
<PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => onPinClick(entry.id, entry.location)} className={'mb-1 mr-1'}/>
|
||||
<EntryMenu
|
||||
id={entry.id}
|
||||
title={entry.title}
|
||||
location={entry.location}
|
||||
isDark={true}
|
||||
onRemove={() => onRemoveClick(entry.id, entry.location)}
|
||||
onDelete={() => onRemoveClick(entry.id, entry.location)}
|
||||
onDelete={() => onDeleteClick(entry.id, entry.location)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -81,12 +81,9 @@ export const ImportHistoryButton: React.FC<ImportHistoryButtonProps> = ({ onImpo
|
|||
titleI18nKey='landing.history.modal.importHistoryError.title'
|
||||
icon='exclamation-circle'
|
||||
>
|
||||
{fileName !== ''
|
||||
? <h5>
|
||||
<Trans i18nKey={i18nKey} values={{ fileName: fileName }}/>
|
||||
</h5>
|
||||
: <h5><Trans i18nKey={i18nKey}/></h5>
|
||||
}
|
||||
<h5>
|
||||
<Trans i18nKey={i18nKey} values={fileName !== '' ? { fileName: fileName } : {}}/>
|
||||
</h5>
|
||||
</ErrorModal>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -115,21 +115,7 @@ export const History: React.FC = () => {
|
|||
}
|
||||
}, [historyWrite, localHistoryEntries, remoteHistoryEntries, user])
|
||||
|
||||
const deleteClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
if (user) {
|
||||
deleteNote(entryId)
|
||||
.then(() => {
|
||||
if (location === HistoryEntryOrigin.LOCAL) {
|
||||
setLocalHistoryEntries(entries => entries.filter(entry => entry.id !== entryId))
|
||||
} else if (location === HistoryEntryOrigin.REMOTE) {
|
||||
setRemoteHistoryEntries(entries => entries.filter(entry => entry.id !== entryId))
|
||||
}
|
||||
})
|
||||
.catch(() => setError('deleteNote'))
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const removeClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
const removeFromHistoryClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
if (location === HistoryEntryOrigin.LOCAL) {
|
||||
setLocalHistoryEntries((entries) => entries.filter(entry => entry.id !== entryId))
|
||||
} else if (location === HistoryEntryOrigin.REMOTE) {
|
||||
|
@ -139,6 +125,16 @@ export const History: React.FC = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const deleteNoteClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
if (user) {
|
||||
deleteNote(entryId)
|
||||
.then(() => {
|
||||
removeFromHistoryClick(entryId, location)
|
||||
})
|
||||
.catch(() => setError('deleteNote'))
|
||||
}
|
||||
}, [user, removeFromHistoryClick])
|
||||
|
||||
const pinClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
if (location === HistoryEntryOrigin.LOCAL) {
|
||||
setLocalHistoryEntries((entries) => {
|
||||
|
@ -216,8 +212,8 @@ export const History: React.FC = () => {
|
|||
viewState={toolbarState.viewState}
|
||||
entries={entriesToShow}
|
||||
onPinClick={pinClick}
|
||||
onRemoveClick={removeClick}
|
||||
onDeleteClick={deleteClick}
|
||||
onRemoveClick={removeFromHistoryClick}
|
||||
onDeleteClick={deleteNoteClick}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue