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:
Erik Michelson 2020-07-01 23:28:49 +02:00 committed by GitHub
parent b23a73ac51
commit e1e8a76fda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 136 additions and 43 deletions

View file

@ -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

View file

@ -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": {

View file

@ -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} />
)
}

View file

@ -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>
)
}

View file

@ -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>
)

View file

@ -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} />
)
}

View file

@ -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>

View file

@ -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>

View file

@ -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>
)

View file

@ -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>
)