mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 01:36:22 +00:00
Add trash can
This commit is contained in:
@@ -89,7 +89,8 @@ NoteItem.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isrequired,
|
||||
tags: PropTypes.array,
|
||||
isStarred: PropTypes.bool.isRequired
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
isTrashed: PropTypes.bool.isRequired
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleDragStart: PropTypes.func.isRequired,
|
||||
|
||||
@@ -15,7 +15,7 @@ import styles from './SideNavFilter.styl'
|
||||
*/
|
||||
const SideNavFilter = ({
|
||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||
isStarredActive, handleStarredButtonClick
|
||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick
|
||||
}) => (
|
||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
|
||||
@@ -30,6 +30,12 @@ const SideNavFilter = ({
|
||||
<i className='fa fa-star fa-fw' />
|
||||
<span styleName='menu-button-label'>Starred</span>
|
||||
</button>
|
||||
<button styleName={isTrashedActive ? 'menu-button--active' : 'menu-button'}
|
||||
onClick={handleTrashedButtonClick}
|
||||
>
|
||||
<i className='fa fa-trash fa-fw' />
|
||||
<span styleName='menu-button-label'>Trashed</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -38,7 +44,9 @@ SideNavFilter.propTypes = {
|
||||
isHomeActive: PropTypes.bool.isRequired,
|
||||
handleAllNotesButtonClick: PropTypes.func.isRequired,
|
||||
isStarredActive: PropTypes.bool.isRequired,
|
||||
handleStarredButtonClick: PropTypes.func.isRequired
|
||||
isTrashedActive: PropTypes.bool.isRequired,
|
||||
handleStarredButtonClick: PropTypes.func.isRequired,
|
||||
handleTrashdButtonClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(SideNavFilter, styles)
|
||||
|
||||
@@ -56,7 +56,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
note: Object.assign({}, nextProps.note)
|
||||
}, () => {
|
||||
this.refs.content.reload()
|
||||
this.refs.tags.reset()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -176,13 +176,27 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleTrashButtonClick (e) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
let { note } = this.state
|
||||
const { isTrashed } = note
|
||||
|
||||
const popupMessage = isTrashed ? 'This work cannot be undone.' : 'Throw it into trashbox.'
|
||||
|
||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
detail: popupMessage,
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
if (dialogueButtonIndex === 0) {
|
||||
if (!isTrashed) {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
} else {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
@@ -195,9 +209,24 @@ class MarkdownNoteDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
}
|
||||
|
||||
handleUndoButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isTrashed = false
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
this.refs.content.reload()
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
|
||||
handleFullScreenButton (e) {
|
||||
@@ -254,12 +283,23 @@ class MarkdownNoteDetail extends React.Component {
|
||||
})
|
||||
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
>
|
||||
<div styleName='info'>
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<div styleName='info-left-top'>
|
||||
<div styleName='info-left-top-folderSelect'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const detailTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
@@ -293,34 +333,30 @@ class MarkdownNoteDetail extends React.Component {
|
||||
>
|
||||
<i className={faClassName} styleName='lock-button' />
|
||||
<span styleName='control-lockButton-tooltip'>
|
||||
{this.state.isLocked ? 'Lock' : 'Unlock'}
|
||||
{this.state.isLocked ? 'Unlock' : 'Lock'}
|
||||
</span>
|
||||
</button>
|
||||
return (
|
||||
this.state.isLockButtonShown ? lockButtonComponent : ''
|
||||
)
|
||||
})()}
|
||||
<TrashButton
|
||||
onClick={(e) => this.handleTrashButtonClick(e)}
|
||||
/>
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<i className='fa fa-expand' styleName='fullScreen-button' />
|
||||
<i className='fa fa-arrows-alt' styleName='fullScreen-button' />
|
||||
</button>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteKey={location.query.key}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
>
|
||||
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
<div styleName='body'>
|
||||
<MarkdownEditor
|
||||
ref='content'
|
||||
|
||||
@@ -55,6 +55,18 @@ $info-margin-under-border = 27px
|
||||
bottom 1px
|
||||
padding-left 30px
|
||||
|
||||
.undo-button
|
||||
position relative
|
||||
border solid 1px transparent
|
||||
line-height 34px
|
||||
vertical-align middle
|
||||
border-radius 2px
|
||||
transition 0.15s
|
||||
user-select none
|
||||
cursor pointer
|
||||
&:hover
|
||||
background-color #D9D9D9
|
||||
|
||||
body[data-theme="dark"]
|
||||
.info
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
@@ -72,7 +72,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
this.refs.tags.reset()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
handleChange (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.tags = this.refs.tags.value
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = findNoteTitle(note.description)
|
||||
@@ -170,13 +170,27 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleTrashButtonClick (e) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
let { note } = this.state
|
||||
const { isTrashed } = note
|
||||
|
||||
const popupMessage = isTrashed ? 'This work cannot be undone.' : 'Throw it into trashbox.'
|
||||
|
||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
detail: popupMessage,
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
if (dialogueButtonIndex === 0) {
|
||||
if (!isTrashed) {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
} else {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
@@ -189,9 +203,23 @@ class SnippetNoteDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
}
|
||||
|
||||
handleUndoButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isTrashed = false
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
|
||||
handleFullScreenButton (e) {
|
||||
@@ -513,13 +541,23 @@ class SnippetNoteDetail extends React.Component {
|
||||
})
|
||||
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='info'>
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<div styleName='info-left-top'>
|
||||
<div styleName='info-left-top-folderSelect'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const detailTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
@@ -541,27 +579,23 @@ class SnippetNoteDetail extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<TrashButton
|
||||
onClick={(e) => this.handleTrashButtonClick(e)}
|
||||
/>
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<i className='fa fa-expand' styleName='fullScreen-button' />
|
||||
<i className='fa fa-arrows-alt' styleName='fullScreen-button' />
|
||||
</button>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteKey={location.query.key}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
<div styleName='body'>
|
||||
<div styleName='description'>
|
||||
<textarea
|
||||
|
||||
@@ -257,6 +257,11 @@ class NoteList extends React.Component {
|
||||
return searchFromNotes(this.props.data, searchInputText)
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/trashed/)) {
|
||||
return data.trashedSet.toJS()
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
}
|
||||
|
||||
let storageKey = params.storageKey
|
||||
let folderKey = params.folderKey
|
||||
let storage = data.storageMap.get(storageKey)
|
||||
@@ -411,6 +416,10 @@ class NoteList extends React.Component {
|
||||
: sortByUpdatedAt
|
||||
this.notes = notes = this.getNotes()
|
||||
.sort(sortFunc)
|
||||
.filter((note) => {
|
||||
// this is for the trash box
|
||||
if (note.isTrashed !== true || location.pathname === '/trashed') return true
|
||||
})
|
||||
|
||||
let noteList = notes
|
||||
.map(note => {
|
||||
|
||||
@@ -33,12 +33,18 @@ class SideNav extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleTrashedButtonClick (e) {
|
||||
let { router } = this.context
|
||||
router.push('/trashed')
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, location, config, dispatch } = this.props
|
||||
|
||||
let isFolded = config.isSideNavFolded
|
||||
let isHomeActive = !!location.pathname.match(/^\/home$/)
|
||||
let isStarredActive = !!location.pathname.match(/^\/starred$/)
|
||||
let isTrashedActive = !!location.pathname.match(/^\/trashed$/)
|
||||
|
||||
let storageList = data.storageMap.map((storage, key) => {
|
||||
return <StorageItem
|
||||
@@ -72,7 +78,9 @@ class SideNav extends React.Component {
|
||||
isHomeActive={isHomeActive}
|
||||
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
|
||||
isStarredActive={isStarredActive}
|
||||
isTrashedActive={isTrashedActive}
|
||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
||||
/>
|
||||
|
||||
<div styleName='storageList'>
|
||||
|
||||
@@ -60,6 +60,7 @@ ReactDOM.render((
|
||||
<Route path='home' />
|
||||
<Route path='starred' />
|
||||
<Route path='searched' />
|
||||
<Route path='trashed' />
|
||||
<Route path='storages'>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path=':storageKey'>
|
||||
|
||||
@@ -10,6 +10,7 @@ function validateInput (input) {
|
||||
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
|
||||
if (!_.isString(input.title)) input.title = ''
|
||||
input.isStarred = !!input.isStarred
|
||||
input.isTrashed = !!input.isTrashed
|
||||
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
|
||||
@@ -21,6 +21,10 @@ function validateInput (input) {
|
||||
validatedInput.isStarred = !!input.isStarred
|
||||
}
|
||||
|
||||
if (input.isTrashed != null) {
|
||||
validatedInput.isTrashed = !!input.isTrashed
|
||||
}
|
||||
|
||||
validatedInput.type = input.type
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
@@ -101,6 +105,7 @@ function updateNote (storageKey, noteKey, input) {
|
||||
noteData.createdAt = new Date()
|
||||
noteData.updatedAt = new Date()
|
||||
noteData.isStarred = false
|
||||
noteData.isTrashed = false
|
||||
noteData.tags = []
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ function defaultDataMap () {
|
||||
starredSet: new Set(),
|
||||
storageNoteMap: new Map(),
|
||||
folderNoteMap: new Map(),
|
||||
tagNoteMap: new Map()
|
||||
tagNoteMap: new Map(),
|
||||
trashedSet: new Set()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +35,10 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.add(uniqueKey)
|
||||
}
|
||||
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
}
|
||||
|
||||
let storageNoteList = state.storageNoteMap.get(note.storage)
|
||||
if (storageNoteList == null) {
|
||||
storageNoteList = new Set(storageNoteList)
|
||||
@@ -78,6 +83,15 @@ function data (state = defaultDataMap(), action) {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
} else {
|
||||
state.trashedSet.delete(uniqueKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Update storageNoteMap if oldNote doesn't exist
|
||||
if (oldNote == null) {
|
||||
state.storageNoteMap = new Map(state.storageNoteMap)
|
||||
@@ -163,6 +177,11 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.delete(originKey)
|
||||
}
|
||||
|
||||
if (originNote.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
state.trashedSet.delete(originKey)
|
||||
}
|
||||
|
||||
// From storageNoteMap
|
||||
state.storageNoteMap = new Map(state.storageNoteMap)
|
||||
let noteSet = state.storageNoteMap.get(originNote.storage)
|
||||
@@ -199,6 +218,15 @@ function data (state = defaultDataMap(), action) {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
} else {
|
||||
state.trashedSet.delete(uniqueKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Update storageNoteMap if oldNote doesn't exist
|
||||
if (oldNote == null) {
|
||||
state.storageNoteMap = new Map(state.storageNoteMap)
|
||||
@@ -283,6 +311,11 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.delete(uniqueKey)
|
||||
}
|
||||
|
||||
if (targetNote.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
state.trashedSet.delete(uniqueKey)
|
||||
}
|
||||
|
||||
// From folderNoteMap
|
||||
let folderKey = targetNote.storage + '-' + targetNote.folder
|
||||
state.folderNoteMap = new Map(state.folderNoteMap)
|
||||
@@ -348,6 +381,11 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.delete(noteKey)
|
||||
}
|
||||
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
state.trashedSet.delete(noteKey)
|
||||
}
|
||||
|
||||
// Delete key from tag map
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
note.tags.forEach((tag) => {
|
||||
|
||||
Reference in New Issue
Block a user