1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 01:36:22 +00:00

Add trash can

This commit is contained in:
asmsuechan
2017-07-12 15:34:40 +09:00
parent ec560ceab1
commit 2650cc2f1c
11 changed files with 294 additions and 141 deletions

View File

@@ -89,7 +89,8 @@ NoteItem.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
title: PropTypes.string.isrequired, title: PropTypes.string.isrequired,
tags: PropTypes.array, tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired
}), }),
handleNoteClick: PropTypes.func.isRequired, handleNoteClick: PropTypes.func.isRequired,
handleDragStart: PropTypes.func.isRequired, handleDragStart: PropTypes.func.isRequired,

View File

@@ -15,7 +15,7 @@ import styles from './SideNavFilter.styl'
*/ */
const SideNavFilter = ({ const SideNavFilter = ({
isFolded, isHomeActive, handleAllNotesButtonClick, isFolded, isHomeActive, handleAllNotesButtonClick,
isStarredActive, handleStarredButtonClick isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick
}) => ( }) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}> <div styleName={isFolded ? 'menu--folded' : 'menu'}>
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'} <button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
@@ -30,6 +30,12 @@ const SideNavFilter = ({
<i className='fa fa-star fa-fw' /> <i className='fa fa-star fa-fw' />
<span styleName='menu-button-label'>Starred</span> <span styleName='menu-button-label'>Starred</span>
</button> </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> </div>
) )
@@ -38,7 +44,9 @@ SideNavFilter.propTypes = {
isHomeActive: PropTypes.bool.isRequired, isHomeActive: PropTypes.bool.isRequired,
handleAllNotesButtonClick: PropTypes.func.isRequired, handleAllNotesButtonClick: PropTypes.func.isRequired,
isStarredActive: PropTypes.bool.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) export default CSSModules(SideNavFilter, styles)

View File

@@ -56,7 +56,7 @@ class MarkdownNoteDetail extends React.Component {
note: Object.assign({}, nextProps.note) note: Object.assign({}, nextProps.note)
}, () => { }, () => {
this.refs.content.reload() this.refs.content.reload()
this.refs.tags.reset() if (this.refs.tags) this.refs.tags.reset()
}) })
} }
} }
@@ -176,30 +176,59 @@ class MarkdownNoteDetail extends React.Component {
} }
handleTrashButtonClick (e) { 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', type: 'warning',
message: 'Delete a note', message: 'Delete a note',
detail: 'This work cannot be undone.', detail: popupMessage,
buttons: ['Confirm', 'Cancel'] buttons: ['Confirm', 'Cancel']
}) })
if (index === 0) { if (dialogueButtonIndex === 0) {
let { note, dispatch } = this.props if (!isTrashed) {
dataApi note.isTrashed = true
.deleteNote(note.storage, note.key)
.then((data) => { this.setState({
let dispatchHandler = () => { note
dispatch({ }, () => {
type: 'DELETE_NOTE', this.save()
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
}) })
} else {
let { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
})
}
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) { handleFullScreenButton (e) {
ee.emit('editor:fullscreen') ee.emit('editor:fullscreen')
} }
@@ -254,72 +283,79 @@ class MarkdownNoteDetail extends React.Component {
}) })
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0] let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
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)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<TagSelect
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
<TodoListPercentage
percentageOfTodo={this.getPercentageOfCompleteTodo(note.content)}
/>
</div>
<div styleName='info-right'>
{(() => {
const faClassName = `fa ${this.getToggleLockButton()}`
const lockButtonComponent =
<button styleName='control-lockButton'
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<i className={faClassName} styleName='lock-button' />
<span styleName='control-lockButton-tooltip'>
{this.state.isLocked ? 'Unlock' : 'Lock'}
</span>
</button>
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
)
})()}
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)}
>
<i className='fa fa-arrows-alt' styleName='fullScreen-button' />
</button>
</div>
</div>
return ( return (
<div className='NoteDetail' <div className='NoteDetail'
style={this.props.style} style={this.props.style}
styleName='root' styleName='root'
> >
<div styleName='info'>
<div styleName='info-left'>
<StarButton styleName='info-left-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<TagSelect {location.pathname === '/trashed' ? trashTopBar : detailTopBar}
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
<TodoListPercentage
percentageOfTodo={this.getPercentageOfCompleteTodo(note.content)}
/>
</div>
<div styleName='info-right'>
{(() => {
const faClassName = `fa ${this.getToggleLockButton()}`
const lockButtonComponent =
<button styleName='control-lockButton'
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<i className={faClassName} styleName='lock-button' />
<span styleName='control-lockButton-tooltip'>
{this.state.isLocked ? 'Lock' : 'Unlock'}
</span>
</button>
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
)
})()}
<TrashButton
onClick={(e) => this.handleTrashButtonClick(e)}
/>
<button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)}
>
<i className='fa fa-expand' 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>
<div styleName='body'> <div styleName='body'>
<MarkdownEditor <MarkdownEditor

View File

@@ -55,6 +55,18 @@ $info-margin-under-border = 27px
bottom 1px bottom 1px
padding-left 30px 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"] body[data-theme="dark"]
.info .info
border-color $ui-dark-borderColor border-color $ui-dark-borderColor

View File

@@ -72,7 +72,7 @@ class SnippetNoteDetail extends React.Component {
snippets.forEach((snippet, index) => { snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload() 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) { handleChange (e) {
let { note } = this.state 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.description = this.refs.description.value
note.updatedAt = new Date() note.updatedAt = new Date()
note.title = findNoteTitle(note.description) note.title = findNoteTitle(note.description)
@@ -170,30 +170,58 @@ class SnippetNoteDetail extends React.Component {
} }
handleTrashButtonClick (e) { 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', type: 'warning',
message: 'Delete a note', message: 'Delete a note',
detail: 'This work cannot be undone.', detail: popupMessage,
buttons: ['Confirm', 'Cancel'] buttons: ['Confirm', 'Cancel']
}) })
if (index === 0) { if (dialogueButtonIndex === 0) {
let { note, dispatch } = this.props if (!isTrashed) {
dataApi note.isTrashed = true
.deleteNote(note.storage, note.key)
.then((data) => { this.setState({
let dispatchHandler = () => { note
dispatch({ }, () => {
type: 'DELETE_NOTE', this.save()
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
}) })
} else {
let { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
})
}
ee.emit('list:next')
} }
} }
handleUndoButtonClick (e) {
let { note } = this.state
note.isTrashed = false
this.setState({
note
}, () => {
this.save()
ee.emit('list:next')
})
}
handleFullScreenButton (e) { handleFullScreenButton (e) {
ee.emit('editor:fullscreen') ee.emit('editor:fullscreen')
} }
@@ -513,54 +541,60 @@ class SnippetNoteDetail extends React.Component {
}) })
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0] let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
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)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<TagSelect
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
<div styleName='info-right'>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)}
>
<i className='fa fa-arrows-alt' styleName='fullScreen-button' />
</button>
</div>
</div>
return ( return (
<div className='NoteDetail' <div className='NoteDetail'
style={this.props.style} style={this.props.style}
styleName='root' styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
> >
<div styleName='info'> {location.pathname === '/trashed' ? trashTopBar : detailTopBar}
<div styleName='info-left'>
<StarButton styleName='info-left-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<TagSelect
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
<div styleName='info-right'>
<TrashButton
onClick={(e) => this.handleTrashButtonClick(e)}
/>
<button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)}
>
<i className='fa fa-expand' 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>
<div styleName='body'> <div styleName='body'>
<div styleName='description'> <div styleName='description'>

View File

@@ -257,6 +257,11 @@ class NoteList extends React.Component {
return searchFromNotes(this.props.data, searchInputText) 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 storageKey = params.storageKey
let folderKey = params.folderKey let folderKey = params.folderKey
let storage = data.storageMap.get(storageKey) let storage = data.storageMap.get(storageKey)
@@ -411,6 +416,10 @@ class NoteList extends React.Component {
: sortByUpdatedAt : sortByUpdatedAt
this.notes = notes = this.getNotes() this.notes = notes = this.getNotes()
.sort(sortFunc) .sort(sortFunc)
.filter((note) => {
// this is for the trash box
if (note.isTrashed !== true || location.pathname === '/trashed') return true
})
let noteList = notes let noteList = notes
.map(note => { .map(note => {

View File

@@ -33,12 +33,18 @@ class SideNav extends React.Component {
}) })
} }
handleTrashedButtonClick (e) {
let { router } = this.context
router.push('/trashed')
}
render () { render () {
let { data, location, config, dispatch } = this.props let { data, location, config, dispatch } = this.props
let isFolded = config.isSideNavFolded let isFolded = config.isSideNavFolded
let isHomeActive = !!location.pathname.match(/^\/home$/) let isHomeActive = !!location.pathname.match(/^\/home$/)
let isStarredActive = !!location.pathname.match(/^\/starred$/) let isStarredActive = !!location.pathname.match(/^\/starred$/)
let isTrashedActive = !!location.pathname.match(/^\/trashed$/)
let storageList = data.storageMap.map((storage, key) => { let storageList = data.storageMap.map((storage, key) => {
return <StorageItem return <StorageItem
@@ -72,7 +78,9 @@ class SideNav extends React.Component {
isHomeActive={isHomeActive} isHomeActive={isHomeActive}
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)} handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
isStarredActive={isStarredActive} isStarredActive={isStarredActive}
isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)} handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
/> />
<div styleName='storageList'> <div styleName='storageList'>

View File

@@ -60,6 +60,7 @@ ReactDOM.render((
<Route path='home' /> <Route path='home' />
<Route path='starred' /> <Route path='starred' />
<Route path='searched' /> <Route path='searched' />
<Route path='trashed' />
<Route path='storages'> <Route path='storages'>
<IndexRedirect to='/home' /> <IndexRedirect to='/home' />
<Route path=':storageKey'> <Route path=':storageKey'>

View File

@@ -10,6 +10,7 @@ function validateInput (input) {
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0) input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
if (!_.isString(input.title)) input.title = '' if (!_.isString(input.title)) input.title = ''
input.isStarred = !!input.isStarred input.isStarred = !!input.isStarred
input.isTrashed = !!input.isTrashed
switch (input.type) { switch (input.type) {
case 'MARKDOWN_NOTE': case 'MARKDOWN_NOTE':

View File

@@ -21,6 +21,10 @@ function validateInput (input) {
validatedInput.isStarred = !!input.isStarred validatedInput.isStarred = !!input.isStarred
} }
if (input.isTrashed != null) {
validatedInput.isTrashed = !!input.isTrashed
}
validatedInput.type = input.type validatedInput.type = input.type
switch (input.type) { switch (input.type) {
case 'MARKDOWN_NOTE': case 'MARKDOWN_NOTE':
@@ -101,6 +105,7 @@ function updateNote (storageKey, noteKey, input) {
noteData.createdAt = new Date() noteData.createdAt = new Date()
noteData.updatedAt = new Date() noteData.updatedAt = new Date()
noteData.isStarred = false noteData.isStarred = false
noteData.isTrashed = false
noteData.tags = [] noteData.tags = []
} }

View File

@@ -11,7 +11,8 @@ function defaultDataMap () {
starredSet: new Set(), starredSet: new Set(),
storageNoteMap: new Map(), storageNoteMap: new Map(),
folderNoteMap: 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) state.starredSet.add(uniqueKey)
} }
if (note.isTrashed) {
state.trashedSet.add(uniqueKey)
}
let storageNoteList = state.storageNoteMap.get(note.storage) let storageNoteList = state.storageNoteMap.get(note.storage)
if (storageNoteList == null) { if (storageNoteList == null) {
storageNoteList = new Set(storageNoteList) 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 // Update storageNoteMap if oldNote doesn't exist
if (oldNote == null) { if (oldNote == null) {
state.storageNoteMap = new Map(state.storageNoteMap) state.storageNoteMap = new Map(state.storageNoteMap)
@@ -163,6 +177,11 @@ function data (state = defaultDataMap(), action) {
state.starredSet.delete(originKey) state.starredSet.delete(originKey)
} }
if (originNote.isTrashed) {
state.trashedSet = new Set(state.trashedSet)
state.trashedSet.delete(originKey)
}
// From storageNoteMap // From storageNoteMap
state.storageNoteMap = new Map(state.storageNoteMap) state.storageNoteMap = new Map(state.storageNoteMap)
let noteSet = state.storageNoteMap.get(originNote.storage) 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 // Update storageNoteMap if oldNote doesn't exist
if (oldNote == null) { if (oldNote == null) {
state.storageNoteMap = new Map(state.storageNoteMap) state.storageNoteMap = new Map(state.storageNoteMap)
@@ -283,6 +311,11 @@ function data (state = defaultDataMap(), action) {
state.starredSet.delete(uniqueKey) state.starredSet.delete(uniqueKey)
} }
if (targetNote.isTrashed) {
state.trashedSet = new Set(state.trashedSet)
state.trashedSet.delete(uniqueKey)
}
// From folderNoteMap // From folderNoteMap
let folderKey = targetNote.storage + '-' + targetNote.folder let folderKey = targetNote.storage + '-' + targetNote.folder
state.folderNoteMap = new Map(state.folderNoteMap) state.folderNoteMap = new Map(state.folderNoteMap)
@@ -348,6 +381,11 @@ function data (state = defaultDataMap(), action) {
state.starredSet.delete(noteKey) state.starredSet.delete(noteKey)
} }
if (note.isTrashed) {
state.trashedSet = new Set(state.trashedSet)
state.trashedSet.delete(noteKey)
}
// Delete key from tag map // Delete key from tag map
state.tagNoteMap = new Map(state.tagNoteMap) state.tagNoteMap = new Map(state.tagNoteMap)
note.tags.forEach((tag) => { note.tags.forEach((tag) => {