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 - 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 - 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 - 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 - CodiMD instances can now be branded either with a '@ <custom string>' or '@ <custom logo>' after the CodiMD logo and text
### Changed ### Changed

View file

@ -70,6 +70,12 @@
"textWithFile": "While trying to import history from '{{fileName}}' an error occurred.", "textWithFile": "While trying to import history from '{{fileName}}' an error occurred.",
"textWithoutFile": "You did not provide any files to upload the history from.", "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." "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": { "tableHeader": {
@ -252,7 +258,8 @@
"deleteNote": { "deleteNote": {
"title": "Delete note", "title": "Delete note",
"question": "Do you really want to delete this 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": { "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 React from 'react'
import { Dropdown } from 'react-bootstrap' import { Dropdown } from 'react-bootstrap'
import { Trans } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../../../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../../../common/show-if/show-if' import { ShowIf } from '../../../../../common/show-if/show-if'
import { HistoryEntryOrigin } from '../history' import { HistoryEntryOrigin } from '../../history'
import './entry-menu.scss' import './entry-menu.scss'
import { DeleteNoteItem } from './delete-note-item'
import { RemoveNoteEntryItem } from './remove-note-entry-item'
export interface EntryMenuProps { export interface EntryMenuProps {
id: string; id: string;
title: string
location: HistoryEntryOrigin location: HistoryEntryOrigin
isDark: boolean; isDark: boolean;
onRemove: () => void onRemove: () => void
@ -15,7 +18,9 @@ export interface EntryMenuProps {
className?: string 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 ( return (
<Dropdown className={`d-inline-flex ${className || ''}`}> <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'> <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"/> <Trans i18nKey="landing.history.menu.entryRemote"/>
</Dropdown.Item> </Dropdown.Item>
</ShowIf> </ShowIf>
<Dropdown.Item onClick={onRemove}> <RemoveNoteEntryItem onConfirm={onRemove} noteTitle={title} />
<ForkAwesomeIcon icon="archive" fixedWidth={true} className="mx-2"/>
<Trans i18nKey="landing.history.menu.removeEntry"/>
</Dropdown.Item>
<Dropdown.Divider/> <Dropdown.Divider/>
<Dropdown.Item onClick={onDelete}> <DeleteNoteItem onConfirm={onDelete} noteTitle={title} />
<ForkAwesomeIcon icon="trash" fixedWidth={true} className="mx-2"/>
<Trans i18nKey="landing.history.menu.deleteNote"/>
</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </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 { Link } from 'react-router-dom'
import { formatHistoryDate } from '../../../../../utils/historyUtils' import { formatHistoryDate } from '../../../../../utils/historyUtils'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' 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 { PinButton } from '../common/pin-button'
import { HistoryEntryProps } from '../history-content/history-content' import { HistoryEntryProps } from '../history-content/history-content'
import './history-card.scss' import './history-card.scss'
export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick }) => { export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick, onDeleteClick }) => {
return ( return (
<div className="p-2 col-xs-12 col-sm-6 col-md-6 col-lg-4"> <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'}> <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'}> <div className={'d-flex flex-column'}>
<EntryMenu <EntryMenu
id={entry.id} id={entry.id}
title={entry.title}
location={entry.location} location={entry.location}
isDark={false} isDark={false}
onRemove={() => onRemoveClick(entry.id, entry.location)} onRemove={() => onRemoveClick(entry.id, entry.location)}
onDelete={() => onRemoveClick(entry.id, entry.location)} onDelete={() => onDeleteClick(entry.id, entry.location)}
/> />
</div> </div>
</Card.Body> </Card.Body>

View file

@ -2,11 +2,11 @@ import React from 'react'
import { Badge } from 'react-bootstrap' import { Badge } from 'react-bootstrap'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { formatHistoryDate } from '../../../../../utils/historyUtils' 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 { PinButton } from '../common/pin-button'
import { HistoryEntryProps } from '../history-content/history-content' 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 ( return (
<tr> <tr>
<td> <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'}/> <PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => onPinClick(entry.id, entry.location)} className={'mb-1 mr-1'}/>
<EntryMenu <EntryMenu
id={entry.id} id={entry.id}
title={entry.title}
location={entry.location} location={entry.location}
isDark={true} isDark={true}
onRemove={() => onRemoveClick(entry.id, entry.location)} onRemove={() => onRemoveClick(entry.id, entry.location)}
onDelete={() => onRemoveClick(entry.id, entry.location)} onDelete={() => onDeleteClick(entry.id, entry.location)}
/> />
</td> </td>
</tr> </tr>

View file

@ -81,12 +81,9 @@ export const ImportHistoryButton: React.FC<ImportHistoryButtonProps> = ({ onImpo
titleI18nKey='landing.history.modal.importHistoryError.title' titleI18nKey='landing.history.modal.importHistoryError.title'
icon='exclamation-circle' icon='exclamation-circle'
> >
{fileName !== '' <h5>
? <h5> <Trans i18nKey={i18nKey} values={fileName !== '' ? { fileName: fileName } : {}}/>
<Trans i18nKey={i18nKey} values={{ fileName: fileName }}/> </h5>
</h5>
: <h5><Trans i18nKey={i18nKey}/></h5>
}
</ErrorModal> </ErrorModal>
</div> </div>
) )

View file

@ -115,21 +115,7 @@ export const History: React.FC = () => {
} }
}, [historyWrite, localHistoryEntries, remoteHistoryEntries, user]) }, [historyWrite, localHistoryEntries, remoteHistoryEntries, user])
const deleteClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => { const removeFromHistoryClick = 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 => {
if (location === HistoryEntryOrigin.LOCAL) { if (location === HistoryEntryOrigin.LOCAL) {
setLocalHistoryEntries((entries) => entries.filter(entry => entry.id !== entryId)) setLocalHistoryEntries((entries) => entries.filter(entry => entry.id !== entryId))
} else if (location === HistoryEntryOrigin.REMOTE) { } 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 => { const pinClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
if (location === HistoryEntryOrigin.LOCAL) { if (location === HistoryEntryOrigin.LOCAL) {
setLocalHistoryEntries((entries) => { setLocalHistoryEntries((entries) => {
@ -216,8 +212,8 @@ export const History: React.FC = () => {
viewState={toolbarState.viewState} viewState={toolbarState.viewState}
entries={entriesToShow} entries={entriesToShow}
onPinClick={pinClick} onPinClick={pinClick}
onRemoveClick={removeClick} onRemoveClick={removeFromHistoryClick}
onDeleteClick={deleteClick} onDeleteClick={deleteNoteClick}
/> />
</Fragment> </Fragment>
) )