1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-16 11:15:12 +00:00

Merge pull request #100 from BoostIO/data-api-refactor

Data api refactoring
This commit is contained in:
Dick Choi
2016-09-08 16:50:43 +09:00
committed by GitHub
52 changed files with 2575 additions and 1249 deletions

View File

@@ -34,7 +34,7 @@ class NoteItem extends React.Component {
? 'root--active' ? 'root--active'
: 'root' : 'root'
} }
key={note.uniqueKey} key={note.storage + '-' + note.key}
onClick={(e) => this.handleClick(e)} onClick={(e) => this.handleClick(e)}
> >
<div styleName='border'/> <div styleName='border'/>

View File

@@ -64,7 +64,7 @@ class NoteList extends React.Component {
return ( return (
<NoteItem <NoteItem
note={note} note={note}
key={`${note.storage}-${note.folder}-${note.key}`} key={`${note.storage}-${note.key}`}
storage={storage} storage={storage}
folder={folder} folder={folder}
isActive={index === _index} isActive={index === _index}

87
browser/lib/Mutable.js Normal file
View File

@@ -0,0 +1,87 @@
class MutableMap {
constructor (iterable) {
this._map = new Map(iterable)
}
get (...args) {
return this._map.get(...args)
}
set (...args) {
return this._map.set(...args)
}
delete (...args) {
return this._map.delete(...args)
}
has (...args) {
return this._map.has(...args)
}
clear (...args) {
return this._map.clear(...args)
}
forEach (...args) {
return this._map.forEach(...args)
}
[Symbol.iterator] () {
return this._map[Symbol.iterator]()
}
map (cb) {
let result = []
for (let [key, value] of this._map) {
result.push(cb(value, key))
}
return result
}
}
class MutableSet {
constructor (iterable) {
if (iterable instanceof MutableSet) {
this._set = new Set(iterable._set)
} else {
this._set = new Set(iterable)
}
}
add (...args) {
return this._set.add(...args)
}
delete (...args) {
return this._set.delete(...args)
}
forEach (...args) {
return this._set.forEach(...args)
}
[Symbol.iterator] () {
return this._set[Symbol.iterator]()
}
map (cb) {
let result = []
this._set.forEach(function (value, key) {
result.push(cb(value, key))
})
return result
}
toJS () {
return Array.from(this._set)
}
}
const Mutable = {
Map: MutableMap,
Set: MutableSet
}
module.exports = Mutable

View File

@@ -2,6 +2,6 @@ const crypto = require('crypto')
const _ = require('lodash') const _ = require('lodash')
module.exports = function (length) { module.exports = function (length) {
if (!_.isFinite(length)) length = 6 if (!_.isFinite(length)) length = 10
return crypto.randomBytes(length).toString('hex') return crypto.randomBytes(length).toString('hex')
} }

View File

@@ -184,12 +184,12 @@ class FolderSelect extends React.Component {
} }
render () { render () {
let { className, storages, value } = this.props let { className, data, value } = this.props
let splitted = value.split('-') let splitted = value.split('-')
let storageKey = splitted.shift() let storageKey = splitted.shift()
let folderKey = splitted.shift() let folderKey = splitted.shift()
let options = [] let options = []
storages.forEach((storage, index) => { data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => { storage.folders.forEach((folder) => {
options.push({ options.push({
storage: storage, storage: storage,

View File

@@ -11,19 +11,17 @@ import ee from 'browser/main/lib/eventEmitter'
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
const Menu = remote.Menu const { Menu, MenuItem, dialog } = remote
const MenuItem = remote.MenuItem
class MarkdownNoteDetail extends React.Component { class MarkdownNoteDetail extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
isMovingNote: false,
note: Object.assign({ note: Object.assign({
title: '', title: '',
content: '', content: ''
isMovingNote: false,
isDeleting: false
}, props.note) }, props.note)
} }
this.dispatchTimer = null this.dispatchTimer = null
@@ -35,9 +33,9 @@ class MarkdownNoteDetail extends React.Component {
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) { if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
if (this.saveQueue != null) this.saveNow()
this.setState({ this.setState({
note: Object.assign({}, nextProps.note), note: Object.assign({}, nextProps.note)
isDeleting: false
}, () => { }, () => {
this.refs.content.reload() this.refs.content.reload()
this.refs.tags.reset() this.refs.tags.reset()
@@ -45,6 +43,10 @@ class MarkdownNoteDetail extends React.Component {
} }
} }
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
findTitle (value) { findTitle (value) {
let splitted = value.split('\n') let splitted = value.split('\n')
let title = null let title = null
@@ -91,15 +93,23 @@ class MarkdownNoteDetail extends React.Component {
save () { save () {
clearTimeout(this.saveQueue) clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => { this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
saveNow () {
let { note, dispatch } = this.props let { note, dispatch } = this.props
dispatch({ clearTimeout(this.saveQueue)
type: 'UPDATE_NOTE', this.saveQueue = null
note: this.state.note
})
dataApi dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note) .updateNote(note.storage, note.key, this.state.note)
}, 1000) .then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
} }
handleFolderChange (e) { handleFolderChange (e) {
@@ -110,7 +120,7 @@ class MarkdownNoteDetail extends React.Component {
let newFolderKey = splitted.shift() let newFolderKey = splitted.shift()
dataApi dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey) .moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => { .then((newNote) => {
this.setState({ this.setState({
isMovingNote: true, isMovingNote: true,
@@ -119,13 +129,13 @@ class MarkdownNoteDetail extends React.Component {
let { dispatch, location } = this.props let { dispatch, location } = this.props
dispatch({ dispatch({
type: 'MOVE_NOTE', type: 'MOVE_NOTE',
note: note, originNote: note,
newNote: newNote note: newNote
}) })
hashHistory.replace({ hashHistory.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: newNote.uniqueKey key: newNote.storage + '-' + newNote.key
} }
}) })
this.setState({ this.setState({
@@ -175,34 +185,28 @@ class MarkdownNoteDetail extends React.Component {
} }
handleDeleteMenuClick (e) { handleDeleteMenuClick (e) {
this.setState({ let index = dialog.showMessageBox(remote.getCurrentWindow(), {
isDeleting: true type: 'warning',
}, () => { message: 'Delete a note',
this.refs.deleteConfirmButton.focus() detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
}) })
} if (index === 0) {
handleDeleteConfirmButtonClick (e) {
let { note, dispatch } = this.props let { note, dispatch } = this.props
dataApi dataApi
.removeNote(note.storage, note.folder, note.key) .deleteNote(note.storage, note.key)
.then(() => { .then((data) => {
let dispatchHandler = () => { let dispatchHandler = () => {
dispatch({ dispatch({
type: 'REMOVE_NOTE', type: 'DELETE_NOTE',
note: note storageKey: data.storageKey,
noteKey: data.noteKey
}) })
} }
ee.once('list:moved', dispatchHandler) ee.once('list:moved', dispatchHandler)
ee.emit('list:next') ee.emit('list:next')
ee.emit('list:focus')
}) })
} }
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
} }
handleDeleteKeyDown (e) { handleDeleteKeyDown (e) {
@@ -210,7 +214,7 @@ class MarkdownNoteDetail extends React.Component {
} }
render () { render () {
let { storages, config } = this.props let { data, config } = this.props
let { note } = this.state let { note } = this.state
return ( return (
@@ -218,32 +222,13 @@ class MarkdownNoteDetail extends React.Component {
style={this.props.style} style={this.props.style}
styleName='root' styleName='root'
> >
{this.state.isDeleting <div styleName='info'>
? <div styleName='info'>
<div styleName='info-delete'
tabIndex='-1'
onKeyDown={(e) => this.handleDeleteKeyDown(e)}
>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
ref='deleteConfirmButton'
>Confirm</button>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<div styleName='info-left-top'> <div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect' <FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder} value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder' ref='folder'
storages={storages} data={data}
onChange={(e) => this.handleFolderChange(e)} onChange={(e) => this.handleFolderChange(e)}
/> />
</div> </div>
@@ -280,7 +265,7 @@ class MarkdownNoteDetail extends React.Component {
</button> </button>
</div> </div>
</div> </div>
}
<div styleName='body'> <div styleName='body'>
<MarkdownEditor <MarkdownEditor
ref='content' ref='content'

View File

@@ -12,34 +12,6 @@ $info-height = 75px
border-bottom $ui-border border-bottom $ui-border
background-color $ui-backgroundColor background-color $ui-backgroundColor
.info-delete
height 80px
display flex
.info-delete-message
height 80px
line-height 80px
padding 0 25px
overflow ellipsis
flex 1
.info-delete-confirmButton
margin 25px 5px 0
width 80px
height 30px
border-radius 2px
border none
colorDangerButton()
.info-delete-cancelButton
width 80px
height 30px
margin 25px 5px 0
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left .info-left
float left float left
padding 0 5px padding 0 5px

View File

@@ -24,21 +24,20 @@ function detectModeByFilename (filename) {
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
const Menu = remote.Menu const { Menu, MenuItem, dialog } = remote
const MenuItem = remote.MenuItem
class SnippetNoteDetail extends React.Component { class SnippetNoteDetail extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
isMovingNote: false,
snippetIndex: 0, snippetIndex: 0,
note: Object.assign({ note: Object.assign({
description: '' description: ''
}, props.note, { }, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet)) snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
}), })
isDeleting: false
} }
} }
@@ -48,6 +47,7 @@ class SnippetNoteDetail extends React.Component {
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key) { if (nextProps.note.key !== this.props.note.key) {
if (this.saveQueue != null) this.saveNow()
let nextNote = Object.assign({ let nextNote = Object.assign({
description: '' description: ''
}, nextProps.note, { }, nextProps.note, {
@@ -55,8 +55,7 @@ class SnippetNoteDetail extends React.Component {
}) })
this.setState({ this.setState({
snippetIndex: 0, snippetIndex: 0,
note: nextNote, note: nextNote
isDeleting: false
}, () => { }, () => {
let { snippets } = this.state.note let { snippets } = this.state.note
snippets.forEach((snippet, index) => { snippets.forEach((snippet, index) => {
@@ -67,6 +66,10 @@ class SnippetNoteDetail extends React.Component {
} }
} }
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
findTitle (value) { findTitle (value) {
let splitted = value.split('\n') let splitted = value.split('\n')
let title = null let title = null
@@ -113,15 +116,23 @@ class SnippetNoteDetail extends React.Component {
save () { save () {
clearTimeout(this.saveQueue) clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => { this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
saveNow () {
let { note, dispatch } = this.props let { note, dispatch } = this.props
dispatch({ clearTimeout(this.saveQueue)
type: 'UPDATE_NOTE', this.saveQueue = null
note: this.state.note
})
dataApi dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note) .updateNote(note.storage, note.key, this.state.note)
}, 1000) .then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
} }
handleFolderChange (e) { handleFolderChange (e) {
@@ -132,7 +143,7 @@ class SnippetNoteDetail extends React.Component {
let newFolderKey = splitted.shift() let newFolderKey = splitted.shift()
dataApi dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey) .moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => { .then((newNote) => {
this.setState({ this.setState({
isMovingNote: true, isMovingNote: true,
@@ -141,13 +152,13 @@ class SnippetNoteDetail extends React.Component {
let { dispatch, location } = this.props let { dispatch, location } = this.props
dispatch({ dispatch({
type: 'MOVE_NOTE', type: 'MOVE_NOTE',
note: note, originNote: note,
newNote: newNote note: newNote
}) })
hashHistory.replace({ hashHistory.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: newNote.uniqueKey key: newNote.storage + '-' + newNote.key
} }
}) })
this.setState({ this.setState({
@@ -198,33 +209,28 @@ class SnippetNoteDetail extends React.Component {
} }
handleDeleteMenuClick (e) { handleDeleteMenuClick (e) {
this.setState({ let index = dialog.showMessageBox(remote.getCurrentWindow(), {
isDeleting: true type: 'warning',
}, () => { message: 'Delete a note',
this.refs.deleteConfirmButton.focus() detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
}) })
} if (index === 0) {
handleDeleteConfirmButtonClick (e) {
let { note, dispatch } = this.props let { note, dispatch } = this.props
dataApi dataApi
.removeNote(note.storage, note.folder, note.key) .deleteNote(note.storage, note.key)
.then(() => { .then((data) => {
let dispatchHandler = () => { let dispatchHandler = () => {
dispatch({ dispatch({
type: 'REMOVE_NOTE', type: 'DELETE_NOTE',
note: note storageKey: data.storageKey,
noteKey: data.noteKey
}) })
} }
ee.once('list:moved', dispatchHandler) ee.once('list:moved', dispatchHandler)
ee.emit('list:next') ee.emit('list:next')
}) })
} }
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
} }
handleTabPlusButtonClick (e) { handleTabPlusButtonClick (e) {
@@ -321,7 +327,7 @@ class SnippetNoteDetail extends React.Component {
} }
render () { render () {
let { storages, config } = this.props let { data, config } = this.props
let { note } = this.state let { note } = this.state
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
@@ -410,31 +416,13 @@ class SnippetNoteDetail extends React.Component {
style={this.props.style} style={this.props.style}
styleName='root' styleName='root'
> >
{this.state.isDeleting <div styleName='info'>
? <div styleName='info'>
<div styleName='info-delete'
tabIndex='-1'
onKeyDown={(e) => this.handleDeleteKeyDown(e)}
>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
ref='deleteConfirmButton'
>Confirm</button>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<div styleName='info-left-top'> <div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect' <FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder} value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder' ref='folder'
storages={storages} data={data}
onChange={(e) => this.handleFolderChange(e)} onChange={(e) => this.handleFolderChange(e)}
/> />
</div> </div>
@@ -471,7 +459,6 @@ class SnippetNoteDetail extends React.Component {
</button> </button>
</div> </div>
</div> </div>
}
<div styleName='body'> <div styleName='body'>
<div styleName='body-description'> <div styleName='body-description'>

View File

@@ -12,34 +12,6 @@ $info-height = 75px
border-bottom $ui-border border-bottom $ui-border
background-color $ui-backgroundColor background-color $ui-backgroundColor
.info-delete
height 80px
display flex
.info-delete-message
height 80px
line-height 80px
padding 0 25px
overflow ellipsis
flex 1
.info-delete-confirmButton
margin 25px 5px 0
width 80px
height 30px
border-radius 2px
border none
colorDangerButton()
.info-delete-cancelButton
width 80px
height 30px
margin 25px 5px 0
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left .info-left
float left float left
padding 0 5px padding 0 5px

View File

@@ -31,19 +31,14 @@ class Detail extends React.Component {
} }
render () { render () {
let { location, notes, config } = this.props let { location, data, config } = this.props
let note = null let note = null
if (location.query.key != null) { if (location.query.key != null) {
let splitted = location.query.key.split('-') let splitted = location.query.key.split('-')
let storageKey = splitted.shift() let storageKey = splitted.shift()
let folderKey = splitted.shift()
let noteKey = splitted.shift() let noteKey = splitted.shift()
note = _.find(notes, { note = data.noteMap.get(storageKey + '-' + noteKey)
storage: storageKey,
folder: folderKey,
key: noteKey
})
} }
if (note == null) { if (note == null) {
@@ -67,7 +62,7 @@ class Detail extends React.Component {
ref='root' ref='root'
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'storages', 'data',
'style', 'style',
'ignorePreviewPointerEvents', 'ignorePreviewPointerEvents',
'location' 'location'
@@ -83,7 +78,7 @@ class Detail extends React.Component {
ref='root' ref='root'
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'storages', 'data',
'style', 'style',
'ignorePreviewPointerEvents', 'ignorePreviewPointerEvents',
'location' 'location'
@@ -95,7 +90,6 @@ class Detail extends React.Component {
Detail.propTypes = { Detail.propTypes = {
dispatch: PropTypes.func, dispatch: PropTypes.func,
storages: PropTypes.array,
style: PropTypes.shape({ style: PropTypes.shape({
left: PropTypes.number left: PropTypes.number
}), }),

View File

@@ -101,7 +101,7 @@ class Main extends React.Component {
<SideNav <SideNav
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'storages', 'data',
'config', 'config',
'location' 'location'
])} ])}
@@ -112,9 +112,8 @@ class Main extends React.Component {
<TopBar style={{width: this.state.listWidth}} <TopBar style={{width: this.state.listWidth}}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'storages',
'config', 'config',
'notes', 'data',
'params', 'params',
'location' 'location'
])} ])}
@@ -122,8 +121,7 @@ class Main extends React.Component {
<NoteList style={{width: this.state.listWidth}} <NoteList style={{width: this.state.listWidth}}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'storages', 'data',
'notes',
'config', 'config',
'params', 'params',
'location' 'location'
@@ -140,8 +138,7 @@ class Main extends React.Component {
style={{left: this.state.listWidth + 1}} style={{left: this.state.listWidth + 1}}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'storages', 'data',
'notes',
'config', 'config',
'params', 'params',
'location' 'location'
@@ -159,7 +156,7 @@ class Main extends React.Component {
Main.propTypes = { Main.propTypes = {
dispatch: PropTypes.func, dispatch: PropTypes.func,
repositories: PropTypes.array data: PropTypes.shape({}).isRequired
} }
export default connect((x) => x)(CSSModules(Main, styles)) export default connect((x) => x)(CSSModules(Main, styles))

View File

@@ -43,7 +43,7 @@ class NoteList extends React.Component {
router.replace({ router.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: this.notes[0].uniqueKey key: this.notes[0].storage + '-' + this.notes[0].key
} }
}) })
return return
@@ -52,7 +52,7 @@ class NoteList extends React.Component {
// Auto scroll // Auto scroll
if (_.isString(location.query.key)) { if (_.isString(location.query.key)) {
let targetIndex = _.findIndex(this.notes, (note) => { let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key return note != null && note.storage + '-' + note.key === location.query.key
}) })
if (targetIndex > -1) { if (targetIndex > -1) {
let list = this.refs.root let list = this.refs.root
@@ -78,7 +78,7 @@ class NoteList extends React.Component {
let { location } = this.props let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => { let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key return note.storage + '-' + note.key === location.query.key
}) })
if (targetIndex === 0) { if (targetIndex === 0) {
@@ -90,7 +90,7 @@ class NoteList extends React.Component {
router.push({ router.push({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: this.notes[targetIndex].uniqueKey key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
} }
}) })
} }
@@ -103,7 +103,7 @@ class NoteList extends React.Component {
let { location } = this.props let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => { let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key return note.storage + '-' + note.key === location.query.key
}) })
if (targetIndex === this.notes.length - 1) { if (targetIndex === this.notes.length - 1) {
@@ -117,7 +117,7 @@ class NoteList extends React.Component {
router.push({ router.push({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: this.notes[targetIndex].uniqueKey key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
} }
}) })
ee.emit('list:moved') ee.emit('list:moved')
@@ -153,30 +153,36 @@ class NoteList extends React.Component {
} }
getNotes () { getNotes () {
let { storages, notes, params, location } = this.props let { data, params, location } = this.props
if (location.pathname.match(/\/home/)) { if (location.pathname.match(/\/home/)) {
return notes return data.noteMap.map((note) => note)
} }
if (location.pathname.match(/\/starred/)) { if (location.pathname.match(/\/starred/)) {
return notes return data.starredSet.toJS()
.filter((note) => note.isStarred) .map((uniqueKey) => data.noteMap.get(uniqueKey))
} }
let storageKey = params.storageKey let storageKey = params.storageKey
let folderKey = params.folderKey let folderKey = params.folderKey
let storage = _.find(storages, {key: storageKey}) let storage = data.storageMap.get(storageKey)
if (storage == null) return [] if (storage == null) return []
let folder = _.find(storage.folders, {key: folderKey}) let folder = _.find(storage.folders, {key: folderKey})
if (folder == null) { if (folder == null) {
return notes return data.storageNoteMap
.filter((note) => note.storage === storageKey) .get(storage.key)
.map((uniqueKey) => data.noteMap.get(uniqueKey))
} }
return notes let folderNoteKeyList = data.folderNoteMap
.filter((note) => note.folder === folderKey) .get(storage.key + '-' + folder.key)
return folderNoteKeyList != null
? folderNoteKeyList
.map((uniqueKey) => data.noteMap.get(uniqueKey))
: []
} }
handleNoteClick (uniqueKey) { handleNoteClick (uniqueKey) {
@@ -194,13 +200,14 @@ class NoteList extends React.Component {
} }
render () { render () {
let { location, storages, notes } = this.props let { location, data, notes } = this.props
this.notes = notes = this.getNotes() this.notes = notes = this.getNotes()
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)) .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
let noteList = notes let noteList = notes
.map((note) => { .map((note) => {
let storage = _.find(storages, {key: note.storage}) if (note == null) return null
let storage = data.storageMap.get(note.storage)
let folder = _.find(storage.folders, {key: note.folder}) let folder = _.find(storage.folders, {key: note.folder})
let tagElements = _.isArray(note.tags) let tagElements = _.isArray(note.tags)
? note.tags.map((tag) => { ? note.tags.map((tag) => {
@@ -212,14 +219,14 @@ class NoteList extends React.Component {
) )
}) })
: [] : []
let isActive = location.query.key === note.uniqueKey let isActive = location.query.key === note.storage + '-' + note.key
return ( return (
<div styleName={isActive <div styleName={isActive
? 'item--active' ? 'item--active'
: 'item' : 'item'
} }
key={note.uniqueKey} key={note.storage + '-' + note.key}
onClick={(e) => this.handleNoteClick(note.uniqueKey)(e)} onClick={(e) => this.handleNoteClick(note.storage + '-' + note.key)(e)}
> >
<div styleName='item-border'/> <div styleName='item-border'/>
<div styleName='item-info'> <div styleName='item-info'>

View File

@@ -36,12 +36,13 @@ class SideNav extends React.Component {
} }
render () { render () {
let { storages, location, config } = this.props let { data, location, config } = 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 storageList = storages.map((storage) => {
let storageList = data.storageMap.map((storage, key) => {
return <StorageItem return <StorageItem
key={storage.key} key={storage.key}
storage={storage} storage={storage}

View File

@@ -33,9 +33,16 @@ class TopBar extends React.Component {
} }
handleNewPostButtonClick (e) { handleNewPostButtonClick (e) {
let { storages, params, dispatch, location } = this.props let { data, params, dispatch, location } = this.props
let storage = _.find(storages, {key: params.storageKey}) let storage = data.storageMap.get(params.storageKey)
if (storage == null) storage = storages[0]
// Find first storage
if (storage == null) {
for (let kv of data.storageMap) {
storage = kv[1]
break
}
}
if (storage == null) throw new Error('No storage to create a note') if (storage == null) throw new Error('No storage to create a note')
let folder = _.find(storage.folders, {key: params.folderKey}) let folder = _.find(storage.folders, {key: params.folderKey})
if (folder == null) folder = storage.folders[0] if (folder == null) folder = storage.folders[0]

View File

@@ -1,11 +1,7 @@
const _ = require('lodash') const _ = require('lodash')
const keygen = require('browser/lib/keygen') const keygen = require('browser/lib/keygen')
const sander = require('sander') const resolveStorageData = require('./resolveStorageData')
const path = require('path') const resolveStorageNotes = require('./resolveStorageNotes')
const defaultBoostnoteJSON = {
folders: []
}
/** /**
* @param {Object} * @param {Object}
@@ -42,31 +38,10 @@ function addStorage (input) {
path: input.path path: input.path
} }
const boostnoteJSONPath = path.join(newStorage.path, 'boostnote.json')
return Promise.resolve(newStorage) return Promise.resolve(newStorage)
.then(function resolveBoostnoteJSON () { .then(resolveStorageData)
return sander.readFile(boostnoteJSONPath) .then(function saveMetadataToLocalStorage (resolvedStorage) {
.then(function checkBoostnoteJSONExists (data) { newStorage = resolvedStorage
let parsedData = JSON.parse(data.toString())
if (!_.isArray(parsedData.folders)) throw new Error('`folders` must be array.')
newStorage.folders = parsedData.folders
.filter(function takeOnlyValidKey (folder) {
return _.isString(folder.key)
})
return newStorage
})
.catch(function tryToRewriteNewBoostnoteJSON (err) {
return sander
.writeFile(boostnoteJSONPath, JSON.stringify(defaultBoostnoteJSON))
.then(function () {
newStorage.folders = defaultBoostnoteJSON.folders
return newStorage
})
})
})
.then(function saveMetadataToLocalStorage () {
rawStorages.push({ rawStorages.push({
key: newStorage.key, key: newStorage.key,
type: newStorage.type, type: newStorage.type,
@@ -75,36 +50,9 @@ function addStorage (input) {
}) })
localStorage.setItem('storages', JSON.stringify(rawStorages)) localStorage.setItem('storages', JSON.stringify(rawStorages))
return newStorage
}) })
.then(function fetchNotes () { .then(resolveStorageNotes)
var folderNotes = newStorage.folders
.map(function fetchNotesFromEachFolder (folder) {
var folderDataJSONPath = path.join(newStorage.path, folder.key, 'data.json')
return sander.readFile(folderDataJSONPath)
.then(function parseData (rawData) {
return JSON.parse(rawData)
})
.then(function validateNotes (data) {
if (!_.isArray(data.notes)) throw new Error('Invalid data.json')
return data.notes
.map(function (note) {
note.folder = folder.key
note.storage = newStorage.key
return note
})
})
.catch(function rewriteNotes (err) {
console.error(err)
return []
})
})
return Promise.all(folderNotes)
.then(function reduceFolderNotes (folderNotes) {
return folderNotes.reduce(function (sum, notes) {
return sum.concat(notes)
}, [])
})
})
.then(function returnValue (notes) { .then(function returnValue (notes) {
return { return {
storage: newStorage, storage: newStorage,

View File

@@ -0,0 +1,63 @@
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const CSON = require('season')
/**
* @param {String} storageKey
* @param {Object} input
* ```
* {
* color: String,
* name: String
* }
* ```
*
* @return {Object}
* ```
* {
* storage: Object
* }
* ```
*/
function createFolder (storageKey, input) {
let rawStorages
let targetStorage
try {
if (input == null) throw new Error('No input found.')
if (!_.isString(input.name)) throw new Error('Name must be a string.')
if (!_.isString(input.color)) throw new Error('Color must be a string.')
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(rawStorages, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function createFolder (storage) {
let key = keygen()
while (storage.folders.some((folder) => folder.key === key)) {
key = keygen()
}
let newFolder = {
key,
color: input.color,
name: input.name
}
storage.folders.push(newFolder)
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
return {
storage
}
})
}
module.exports = createFolder

View File

@@ -0,0 +1,84 @@
const sander = require('sander')
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const path = require('path')
const CSON = require('season')
function validateInput (input) {
if (!_.isArray(input.tags)) input.tags = []
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
if (!_.isString(input.title)) input.title = ''
input.isStarred = !!input.isStarred
switch (input.type) {
case 'MARKDOWN_NOTE':
if (!_.isString(input.content)) input.content = ''
break
case 'SNIPPET_NOTE':
if (!_.isString(input.description)) input.description = ''
if (!_.isArray(input.snippets)) {
input.snippets = [{
name: '',
mode: 'text',
content: ''
}]
}
break
default:
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
}
}
function createNote (storageKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
input = Object.assign({}, input)
validateInput(input)
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function checkFolderExists (storage) {
if (_.find(storage.folders, {key: input.folder}) == null) {
throw new Error('Target folder doesn\'t exist.')
}
return storage
})
.then(function saveNote (storage) {
let key = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
key = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
throw err
}
}
}
let noteData = Object.assign({}, input, {
key,
createdAt: new Date(),
updatedAt: new Date(),
storage: storageKey
})
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData
})
}
module.exports = createNote

View File

@@ -0,0 +1,75 @@
const _ = require('lodash')
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const CSON = require('season')
const sander = require('sander')
/**
* @param {String} storageKey
* @param {String} folderKey
*
* @return {Object}
* ```
* {
* storage: Object,
* folderKey: String
* }
* ```
*/
function deleteFolder (storageKey, folderKey) {
let rawStorages
let targetStorage
try {
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(rawStorages, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function assignNotes (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
return {
storage,
notes
}
})
})
.then(function deleteFolderAndNotes (data) {
let { storage, notes } = data
storage.folders = storage.folders
.filter(function excludeTargetFolder (folder) {
return folder.key !== folderKey
})
let targetNotes = notes.filter(function filterTargetNotes (note) {
return note.folder === folderKey
})
let deleteAllNotes = targetNotes
.map(function deleteNote (note) {
const notePath = path.join(storage.path, 'notes', note.key + '.cson')
return sander.unlink(notePath)
.catch(function (err) {
console.warn('Failed to delete', notePath, err)
})
})
return Promise.all(deleteAllNotes)
.then(() => storage)
})
.then(function (storage) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
return {
storage,
folderKey
}
})
}
module.exports = deleteFolder

View File

@@ -0,0 +1,34 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const sander = require('sander')
function deleteNote (storageKey, noteKey) {
let targetStorage
try {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function deleteNoteFile (storage) {
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
sander.unlinkSync(notePath)
} catch (err) {
console.warn('Failed to delete note cson', err)
}
return {
noteKey,
storageKey
}
})
}
module.exports = deleteNote

View File

@@ -1,566 +1,20 @@
const keygen = require('browser/lib/keygen') const dataApi = {
const CSON = require('season') init: require('./init'),
const path = require('path') addStorage: require('./addStorage'),
const _ = require('lodash') renameStorage: require('./renameStorage'),
const sander = require('sander') removeStorage: require('./removeStorage'),
const consts = require('browser/lib/consts') createFolder: require('./createFolder'),
updateFolder: require('./updateFolder'),
deleteFolder: require('./deleteFolder'),
createNote: require('./createNote'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'),
migrateFromV5Storage: require('./migrateFromV5Storage'),
let storages = [] _migrateFromV6Storage: require('./migrateFromV6Storage'),
let notes = [] _resolveStorageData: require('./resolveStorageData'),
_resolveStorageNotes: require('./resolveStorageNotes')
let queuedTasks = []
function queueSaveFolder (storageKey, folderKey) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Failed to queue: Storage doesn\'t exist.')
let targetTasks = queuedTasks.filter((task) => task.storage === storageKey && task.folder === folderKey)
targetTasks.forEach((task) => {
clearTimeout(task.timer)
})
queuedTasks = queuedTasks.filter((task) => task.storage !== storageKey || task.folder !== folderKey)
let newTimer = setTimeout(() => {
let folderNotes = notes.filter((note) => note.storage === storageKey && note.folder === folderKey)
sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({
notes: folderNotes.map((note) => {
let json = note.toJSON()
delete json.storage
return json
})
}))
}, 1500)
queuedTasks.push({
storage: storageKey,
folder: folderKey,
timer: newTimer
})
} }
class Storage { module.exports = dataApi
constructor (cache) {
this.key = cache.key
this.cache = cache
}
loadJSONData () {
return new Promise((resolve, reject) => {
try {
let data = CSON.readFileSync(path.join(this.cache.path, 'boostnote.json'))
this.data = data
resolve(this)
} catch (err) {
reject(err)
}
})
}
toJSON () {
return Object.assign({}, this.cache, this.data)
}
initStorage () {
return this.loadJSONData()
.catch((err) => {
console.error(err.code)
if (err.code === 'ENOENT') {
let initialStorage = {
folders: []
}
return sander.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(initialStorage))
} else throw err
})
.then(() => this.loadJSONData())
}
saveData () {
return sander
.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(this.data))
.then(() => this)
}
saveCache () {
_saveCaches()
}
static forge (cache) {
let instance = new this(cache)
return instance
}
}
class Note {
constructor (note) {
this.storage = note.storage
this.folder = note.folder
this.key = note.key
this.uniqueKey = `${note.storage}-${note.folder}-${note.key}`
this.data = note
}
toJSON () {
return Object.assign({}, this.data, {
uniqueKey: this.uniqueKey
})
}
save () {
let storage = _.find(storages, {key: this.storage})
if (storage == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
let folder = _.find(storage.data.folders, {key: this.folder})
if (folder == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
// FS MUST BE MANIPULATED BY ASYNC METHOD
queueSaveFolder(storage.key, folder.key)
return Promise.resolve(this)
}
static forge (note) {
let instance = new this(note)
return Promise.resolve(instance)
}
}
function init () {
let fetchStorages = function () {
let caches
try {
caches = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(caches)) throw new Error('Cached data is not valid.')
} catch (e) {
throw e
console.error(e)
caches = []
localStorage.setItem('storages', JSON.stringify(caches))
}
return caches.map((cache) => {
return Storage
.forge(cache)
.loadJSONData()
.catch((err) => {
console.error(err)
console.error('Failed to load a storage JSON File: %s', cache)
return null
})
})
}
let fetchNotes = function (storages) {
let notes = []
let modifiedStorages = []
storages
.forEach((storage) => {
storage.data.folders.forEach((folder) => {
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
let data
try {
data = CSON.readFileSync(dataPath)
} catch (e) {
// Remove folder if fetching failed.
console.error('Failed to load data: %s', dataPath)
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
if (modifiedStorages.some((modified) => modified.key === storage.key)) modifiedStorages.push(storage)
return
}
data.notes.forEach((note) => {
note.storage = storage.key
note.folder = folder.key
notes.push(Note.forge(note))
})
})
}, [])
return Promise
.all(modifiedStorages.map((storage) => storage.saveData()))
.then(() => Promise.all(notes))
}
return Promise.all(fetchStorages())
.then((_storages) => {
storages = _storages.filter((storage) => {
if (!_.isObject(storage)) return false
return true
})
_saveCaches()
return storages
})
.then(fetchNotes)
.then((_notes) => {
notes = _notes
return {
storages: storages.map((storage) => storage.toJSON()),
notes: notes.map((note) => note.toJSON())
}
})
}
function _saveCaches () {
localStorage.setItem('storages', JSON.stringify(storages.map((storage) => storage.cache)))
}
function addStorage (input) {
if (!_.isString(input.path)) {
return Promise.reject(new Error('Path must be a string.'))
}
let key = keygen()
while (storages.some((storage) => storage.key === key)) {
key = keygen()
}
return Storage
.forge({
name: input.name,
key: key,
type: input.type,
path: input.path
})
.initStorage()
.then((storage) => {
let _notes = []
let isFolderRemoved = false
storage.data.folders.forEach((folder) => {
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
let data
try {
data = CSON.readFileSync(dataPath)
} catch (e) {
// Remove folder if fetching failed.
console.error('Failed to load data: %s', dataPath)
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
isFolderRemoved = true
return true
}
data.notes.forEach((note) => {
note.storage = storage.key
note.folder = folder.key
_notes.push(Note.forge(note))
})
})
return Promise.all(_notes)
.then((_notes) => {
notes = notes.concat(_notes)
let data = {
storage: storage,
notes: _notes
}
return isFolderRemoved
? storage.saveData().then(() => data)
: data
})
})
.then((data) => {
storages = storages.filter((storage) => storage.key !== data.storage.key)
storages.push(data.storage)
_saveCaches()
if (data.storage.data.folders.length < 1) {
return createFolder(data.storage.key, {
name: 'Default',
color: consts.FOLDER_COLORS[0]
}).then(() => data)
}
return data
})
.then((data) => {
return {
storage: data.storage.toJSON(),
notes: data.notes.map((note) => note.toJSON())
}
})
}
function removeStorage (key) {
storages = storages.filter((storage) => storage.key !== key)
_saveCaches()
notes = notes.filter((note) => note.storage !== key)
return Promise.resolve(true)
}
function renameStorage (key, name) {
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
storage.cache.name = name
storage.saveCache()
return Promise.resolve(storage.toJSON())
}
function migrateFromV5 (key, data) {
let oldFolders = data.folders
let oldArticles = data.articles
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let migrateFolders = oldFolders.map((oldFolder) => {
let folderKey = keygen()
while (storage.data.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
storage.data.folders.push(newFolder)
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
let folderNotes = []
articles.forEach((article) => {
let noteKey = keygen()
while (notes.some((note) => note.storage === key && note.folder === folderKey && note.key === noteKey)) {
key = keygen()
}
if (article.mode === 'markdown') {
let newNote = new Note({
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey
})
notes.push(newNote)
folderNotes.push(newNote)
} else {
let newNote = new Note({
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [{
name: article.mode,
mode: article.mode,
content: article.content
}]
})
notes.push(newNote)
folderNotes.push(newNote)
}
})
return sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({
notes: folderNotes.map((note) => {
let json = note.toJSON()
delete json.storage
return json
})
}))
})
return Promise.all(migrateFolders)
.then(() => storage.saveData())
.then(() => {
return {
storage: storage.toJSON(),
notes: notes.filter((note) => note.storage === key)
.map((note) => note.toJSON())
}
})
}
function createFolder (key, input) {
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folderKey = keygen()
while (storage.data.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: input.name,
color: input.color
}
const defaultData = {notes: []}
// FS MUST BE MANIPULATED BY ASYNC METHOD
return sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify(defaultData))
.then(() => {
storage.data.folders.push(newFolder)
return storage
.saveData()
.then((storage) => storage.toJSON())
})
}
function updateFolder (storageKey, folderKey, input) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folder = _.find(storage.data.folders, {key: folderKey})
folder.color = input.color
folder.name = input.name
return storage
.saveData()
.then((storage) => storage.toJSON())
}
function removeFolder (storageKey, folderKey) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
storage.data.folders = storage.data.folders.filter((folder) => folder.key !== folderKey)
notes = notes.filter((note) => note.storage !== storageKey || note.folder !== folderKey)
// FS MUST BE MANIPULATED BY ASYNC METHOD
return sander
.rimraf(path.join(storage.cache.path, folderKey))
.catch((err) => {
if (err.code === 'ENOENT') return true
else throw err
})
.then(() => storage.saveData())
.then((storage) => storage.toJSON())
}
function createMarkdownNote (storageKey, folderKey, input) {
let key = keygen()
while (notes.some((note) => note.storage === storageKey && note.folder === folderKey && note.key === key)) {
key = keygen()
}
let newNote = new Note(Object.assign({
tags: [],
title: '',
content: ''
}, input, {
type: 'MARKDOWN_NOTE',
storage: storageKey,
folder: folderKey,
key: key,
isStarred: false,
createdAt: new Date(),
updatedAt: new Date()
}))
notes.push(newNote)
return newNote
.save()
.then(() => newNote.toJSON())
}
function createSnippetNote (storageKey, folderKey, input) {
let key = keygen()
while (notes.some((note) => note.storage === storageKey && note.folder === folderKey && note.key === key)) {
key = keygen()
}
let newNote = new Note(Object.assign({
tags: [],
title: '',
description: '',
snippets: [{
name: '',
mode: 'text',
content: ''
}]
}, input, {
type: 'SNIPPET_NOTE',
storage: storageKey,
folder: folderKey,
key: key,
isStarred: false,
createdAt: new Date(),
updatedAt: new Date()
}))
notes.push(newNote)
return newNote
.save()
.then(() => newNote.toJSON())
}
function updateNote (storageKey, folderKey, noteKey, input) {
let note = _.find(notes, {
key: noteKey,
storage: storageKey,
folder: folderKey
})
switch (note.data.type) {
case 'MARKDOWN_NOTE':
note.data.title = input.title
note.data.tags = input.tags
note.data.content = input.content
note.data.updatedAt = input.updatedAt
break
case 'SNIPPET_NOTE':
note.data.title = input.title
note.data.tags = input.tags
note.data.description = input.description
note.data.snippets = input.snippets
note.data.updatedAt = input.updatedAt
}
return note.save()
.then(() => note.toJSON())
}
function removeNote (storageKey, folderKey, noteKey) {
notes = notes.filter((note) => note.storage !== storageKey || note.folder !== folderKey || note.key !== noteKey)
queueSaveFolder(storageKey, folderKey)
return Promise.resolve(null)
}
function moveNote (storageKey, folderKey, noteKey, newStorageKey, newFolderKey) {
let note = _.find(notes, {
key: noteKey,
storage: storageKey,
folder: folderKey
})
if (note == null) throw new Error('Note doesn\'t exist.')
let storage = _.find(storages, {key: newStorageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folder = _.find(storage.data.folders, {key: newFolderKey})
if (folder == null) throw new Error('Folder doesn\'t exist.')
note.storage = storage.key
note.data.storage = storage.key
note.folder = folder.key
note.data.folder = folder.key
let key = note.key
while (notes.some((note) => note.storage === storage.key && note.folder === folder.key && note.key === key)) {
key = keygen()
}
note.key = key
note.data.key = key
note.uniqueKey = `${note.storage}-${note.folder}-${note.key}`
console.log(note.uniqueKey)
queueSaveFolder(storageKey, folderKey)
return note.save()
.then(() => note.toJSON())
}
module.exports = {
init,
addStorage,
removeStorage,
renameStorage,
createFolder,
updateFolder,
removeFolder,
createMarkdownNote,
createSnippetNote,
updateNote,
removeNote,
moveNote,
migrateFromV5
}

View File

@@ -1,8 +1,21 @@
'use strict' 'use strict'
const _ = require('lodash') const _ = require('lodash')
const sander = require('sander') const resolveStorageData = require('./resolveStorageData')
const path = require('path') const resolveStorageNotes = require('./resolveStorageNotes')
/**
* @return {Object} all storages and notes
* ```
* {
* storages: [...],
* notes: [...]
* }
* ```
*
* This method deals with 3 patterns.
* 1. v1
* 2. legacy
* 3. empty directory
*/
function init () { function init () {
let fetchStorages = function () { let fetchStorages = function () {
let rawStorages let rawStorages
@@ -10,67 +23,38 @@ function init () {
rawStorages = JSON.parse(window.localStorage.getItem('storages')) rawStorages = JSON.parse(window.localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.') if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
} catch (e) { } catch (e) {
console.error(e) console.warn('Failed to parse cached data from localStorage', e)
rawStorages = [] rawStorages = []
window.localStorage.setItem('storages', JSON.stringify(rawStorages)) window.localStorage.setItem('storages', JSON.stringify(rawStorages))
} }
return Promise.all(rawStorages return Promise.all(rawStorages
.map(function assignFoldersToStorage (rawStorage) { .map(resolveStorageData))
let data
let boostnoteJSONPath = path.join(rawStorage.path, 'boostnote.json')
try {
data = JSON.parse(sander.readFileSync(boostnoteJSONPath))
if (!_.isArray(data.folders)) throw new Error('folders should be an array.')
rawStorage.folders = data.folders
} catch (err) {
if (err.code === 'ENOENT') {
console.warn('boostnote.json file doesn\'t exist the given path')
} else {
console.error(err)
}
rawStorage.folders = []
}
return Promise.resolve(rawStorage)
}))
} }
let fetchNotes = function (storages) { let fetchNotes = function (storages) {
let notes = [] let findNotesFromEachStorage = storages
.map(resolveStorageNotes)
storages return Promise.all(findNotesFromEachStorage)
.forEach((storage) => { .then(function concatNoteGroup (noteGroups) {
storage.folders.forEach((folder) => { return noteGroups.reduce(function (sum, group) {
let dataPath = path.join(storage.path, folder.key, 'data.json') return sum.concat(group)
let data }, [])
try {
data = JSON.parse(sander.readFileSync(dataPath))
if (!_.isArray(data.notes)) throw new Error('notes should be an array.')
} catch (e) {
// Remove folder if fetching failed.
console.error('Failed to load data: %s', dataPath)
storage.folders = storage.folders.filter((_folder) => _folder.key !== folder.key)
data = {notes: []}
}
data.notes.forEach((note) => {
note.storage = storage.key
note.folder = folder.key
notes.push(note)
}) })
}) .then(function returnData (notes) {
}) return {
return Promise.resolve({
storages, storages,
notes notes
}
}) })
} }
return Promise.resolve(fetchStorages()) return Promise.resolve(fetchStorages())
.then((storages) => { .then((storages) => {
storages = storages.filter((storage) => { return storages
.filter((storage) => {
if (!_.isObject(storage)) return false if (!_.isObject(storage)) return false
return true return true
}) })
return storages
}) })
.then(fetchNotes) .then(fetchNotes)
} }

View File

@@ -0,0 +1,110 @@
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const resolveStorageData = require('./resolveStorageData')
const consts = require('browser/lib/consts')
const CSON = require('season')
const path = require('path')
const sander = require('sander')
function migrateFromV5Storage (storageKey, data) {
let targetStorage
try {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function (storage) {
return importAll(storage, data)
})
}
function importAll (storage, data) {
let oldArticles = data.articles
let notes = []
data.folders
.forEach(function (oldFolder) {
let folderKey = keygen()
while (storage.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
storage.folders.push(newFolder)
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
articles.forEach((article) => {
let noteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
noteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
console.error('Failed to read `notes` directory.')
throw err
}
}
}
if (article.mode === 'markdown') {
let newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey
}
notes.push(newNote)
} else {
let newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [{
name: article.mode,
mode: article.mode,
content: article.content
}]
}
notes.push(newNote)
}
})
})
notes.forEach(function (note) {
CSON.writeFileSync(path.join(storage.path, 'notes', note.key + '.cson'), _.omit(note, ['storage', 'key']))
})
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['version', 'folders']))
return {
storage,
notes
}
}
module.exports = migrateFromV5Storage

View File

@@ -0,0 +1,85 @@
const path = require('path')
const sander = require('sander')
const keygen = require('browser/lib/keygen')
const _ = require('lodash')
const CSON = require('season')
function migrateFromV5Storage (storagePath) {
var boostnoteJSONPath = path.join(storagePath, 'boostnote.json')
return Promise.resolve()
.then(function readBoostnoteJSON () {
return sander.readFile(boostnoteJSONPath, {
encoding: 'utf-8'
})
})
.then(function verifyVersion (rawData) {
var boostnoteJSONData = JSON.parse(rawData)
if (boostnoteJSONData.version === '1.0') throw new Error('Target storage seems to be transformed already.')
if (!_.isArray(boostnoteJSONData.folders)) throw new Error('the value of folders is not an array.')
return boostnoteJSONData
})
.then(function setVersion (boostnoteJSONData) {
boostnoteJSONData.version = '1.0'
return sander.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
.then(() => boostnoteJSONData)
})
.then(function fetchNotes (boostnoteJSONData) {
var fetchNotesFromEachFolder = boostnoteJSONData.folders
.map(function (folder) {
const folderDataJSONPath = path.join(storagePath, folder.key, 'data.json')
return sander
.readFile(folderDataJSONPath, {
encoding: 'utf-8'
})
.then(function (rawData) {
var data = JSON.parse(rawData)
if (!_.isArray(data.notes)) throw new Error('value of notes is not an array.')
return data.notes
.map(function setFolderToNote (note) {
note.folder = folder.key
return note
})
})
.catch(function failedToReadDataJSON (err) {
console.warn('Failed to fetch notes from ', folderDataJSONPath, err)
return []
})
.then(function deleteFolderDir (data) {
sander.rimrafSync(path.join(storagePath, folder.key))
return data
})
})
return Promise.all(fetchNotesFromEachFolder)
.then(function flatten (folderNotes) {
return folderNotes
.reduce(function concatNotes (sum, notes) {
return sum.concat(notes)
}, [])
})
})
.then(function saveNotes (notes) {
notes.forEach(function renewKey (note) {
var newKey = keygen()
while (notes.some((_note) => _note.key === newKey)) {
newKey = keygen()
}
note.key = newKey
})
const noteDirPath = path.join(storagePath, 'notes')
notes
.map(function saveNote (note) {
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
})
return true
})
.catch(function handleError (err) {
console.warn(err)
return false
})
}
module.exports = migrateFromV5Storage

View File

@@ -0,0 +1,90 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const CSON = require('season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
let oldStorage, newStorage
try {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Storage doesn\'t exist.')
oldStorage = _.find(cachedStorageList, {key: storageKey})
if (oldStorage == null) throw new Error('Storage doesn\'t exist.')
newStorage = _.find(cachedStorageList, {key: newStorageKey})
if (newStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(oldStorage)
.then(function saveNote (_oldStorage) {
oldStorage = _oldStorage
let noteData
let notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
throw err
}
let newNoteKey
return Promise.resolve()
.then(function resolveNewStorage () {
if (storageKey === newStorageKey) {
newNoteKey = noteKey
return oldStorage
}
return resolveStorageData(newStorage)
.then(function findNewNoteKey (_newStorage) {
newStorage = _newStorage
newNoteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
newNoteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
throw err
}
}
}
return newStorage
})
})
.then(function checkFolderExistsAndPrepareNoteData (newStorage) {
if (_.find(newStorage.folders, {key: newFolderKey}) == null) throw new Error('Target folder doesn\'t exist.')
noteData.folder = newFolderKey
noteData.key = newNoteKey
noteData.storage = newStorageKey
noteData.updatedAt = new Date()
return noteData
})
.then(function writeAndReturn (noteData) {
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData
})
.then(function deleteOldNote (data) {
if (storageKey !== newStorageKey) {
try {
sander.unlinkSync(path.join(oldStorage.path, 'notes', noteKey + '.cson'))
} catch (err) {
console.warn(err)
}
}
return data
})
})
}
module.exports = moveNote

View File

@@ -0,0 +1,30 @@
const _ = require('lodash')
/**
* @param {String} key
* @return {key}
*/
function removeStorage (key) {
let rawStorages
try {
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('invalid storages')
} catch (e) {
console.warn(e)
rawStorages = []
}
rawStorages = rawStorages
.filter(function excludeTargetStorage (rawStorage) {
return rawStorage.key !== key
})
localStorage.setItem('storages', JSON.stringify(rawStorages))
return Promise.resolve({
storageKey: key
})
}
module.exports = removeStorage

View File

@@ -0,0 +1,29 @@
const _ = require('lodash')
/**
* @param {String} key
* @param {String} name
* @return {Object} Storage meta data
*/
function renameStorage (key, name) {
if (!_.isString(name)) return Promise.reject(new Error('Name must be a string.'))
let cachedStorageList
try {
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
} catch (err) {
console.log('error got')
console.error(err)
return Promise.reject(err)
}
let targetStorage = _.find(cachedStorageList, {key: key})
if (targetStorage == null) return Promise.reject('Storage')
targetStorage.name = name
localStorage.setItem('storages', JSON.stringify(cachedStorageList))
return Promise.resolve(targetStorage)
}
module.exports = renameStorage

View File

@@ -0,0 +1,39 @@
const _ = require('lodash')
const path = require('path')
const CSON = require('season')
const migrateFromV6Storage = require('./migrateFromV6Storage')
function resolveStorageData (storageCache) {
let storage = {
key: storageCache.key,
name: storageCache.name,
type: storageCache.type,
path: storageCache.path
}
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
try {
let jsonData = CSON.readFileSync(boostnoteJSONPath)
if (!_.isArray(jsonData.folders)) throw new Error('folders should be an array.')
storage.folders = jsonData.folders
storage.version = jsonData.version
} catch (err) {
if (err.code === 'ENOENT') {
console.warn('boostnote.json file doesn\'t exist the given path')
CSON.writeFileSync(boostnoteJSONPath, {folders: [], version: '1.0'})
} else {
console.error(err)
}
storage.folders = []
storage.version = '1.0'
}
if (storage.version === '1.0') {
return Promise.resolve(storage)
}
console.log('Transform Legacy storage', storage.path)
return migrateFromV6Storage(storage.path)
.then(() => storage)
}
module.exports = resolveStorageData

View File

@@ -0,0 +1,30 @@
const sander = require('sander')
const path = require('path')
const CSON = require('season')
function resolveStorageNotes (storage) {
const notesDirPath = path.join(storage.path, 'notes')
let notePathList
try {
notePathList = sander.readdirSync(notesDirPath)
} catch (err) {
if (err.code === 'ENOENT') {
console.log(notesDirPath, ' doesn\'t exist.')
sander.mkdirSync(notesDirPath)
} else {
console.warn('Failed to find note dir', notesDirPath, err)
}
notePathList = []
}
let notes = notePathList
.map(function parseCSONFile (notePath) {
let data = CSON.readFileSync(path.join(notesDirPath, notePath))
data.key = path.basename(notePath, '.cson')
data.storage = storage.key
return data
})
return Promise.resolve(notes)
}
module.exports = resolveStorageNotes

View File

@@ -0,0 +1,56 @@
const _ = require('lodash')
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const CSON = require('season')
/**
* @param {String} storageKey
* @param {String} folderKey
* @param {Object} input
* ```
* {
* color: String,
* name: String
* }
* ```
*
* @return {Object}
* ```
* {
* storage: Object
* }
* ```
*/
function updateFolder (storageKey, folderKey, input) {
let rawStorages
let targetStorage
try {
if (input == null) throw new Error('No input found.')
if (!_.isString(input.name)) throw new Error('Name must be a string.')
if (!_.isString(input.color)) throw new Error('Color must be a string.')
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(rawStorages, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function updateFolder (storage) {
let targetFolder = _.find(storage.folders, {key: folderKey})
if (targetFolder == null) throw new Error('Target folder doesn\'t exist.')
targetFolder.name = input.name
targetFolder.color = input.color
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
return {
storage
}
})
}
module.exports = updateFolder

View File

@@ -0,0 +1,122 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const CSON = require('season')
function validateInput (input) {
let validatedInput = {}
if (input.tags != null) {
if (!_.isArray(input.tags)) validatedInput.tags = []
validatedInput.tags = input.tags
.filter((tag) => _.isString(tag) && tag.trim().length > 0)
}
if (input.title != null) {
if (!_.isString(input.title)) validatedInput.title = ''
else validatedInput.title = input.title
}
if (input.isStarred != null) {
validatedInput.isStarred = !!input.isStarred
}
validatedInput.type = input.type
switch (input.type) {
case 'MARKDOWN_NOTE':
if (input.content != null) {
if (!_.isString(input.content)) validatedInput.content = ''
else validatedInput.content = input.content
}
return input
case 'SNIPPET_NOTE':
if (input.description != null) {
if (!_.isString(input.description)) validatedInput.description = ''
else validatedInput.description = input.description
}
if (input.snippets != null) {
if (!_.isArray(input.snippets)) {
validatedInput.snippets = [{
name: '',
mode: 'text',
content: ''
}]
} else {
validatedInput.snippets = input.snippets
}
validatedInput.snippets.filter((snippet) => {
if (!_.isString(snippet.name)) return false
if (!_.isString(snippet.mode)) return false
if (!_.isString(snippet.content)) return false
return true
})
}
return validatedInput
default:
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
}
}
function updateNote (storageKey, noteKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
input = validateInput(input)
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function saveNote (storage) {
let noteData
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
noteData = input.type === 'SNIPPET_NOTE'
? {
type: 'SNIPPET_NOTE',
description: [],
snippets: [{
name: '',
mode: 'text',
content: ''
}]
}
: {
type: 'MARKDOWN_NOTE',
content: ''
}
noteData.title = ''
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
noteData.folder = storage.folders[0].key
noteData.createdAt = new Date()
noteData.updatedAt = new Date()
noteData.isStarred = false
noteData.tags = []
}
if (noteData.type === 'SNIPPET_NOTE') {
noteData.title
}
Object.assign(noteData, input, {
key: noteKey,
updatedAt: new Date(),
storage: storageKey
})
CSON.writeFileSync(path.join(storage.path, 'notes', noteKey + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData
})
}
module.exports = updateNote

View File

@@ -24,23 +24,26 @@ class NewNoteModal extends React.Component {
handleMarkdownNoteButtonClick (e) { handleMarkdownNoteButtonClick (e) {
let { storage, folder, dispatch, location } = this.props let { storage, folder, dispatch, location } = this.props
dataApi dataApi
.createMarkdownNote(storage, folder, { .createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '', title: '',
content: '' content: ''
}) })
.then((note) => { .then((note) => {
dispatch({ dispatch({
type: 'CREATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
hashHistory.push({ hashHistory.push({
pathname: location.pathname, pathname: location.pathname,
query: {key: note.uniqueKey} query: {key: note.storage + '-' + note.key}
}) })
ee.emit('detail:focus') ee.emit('detail:focus')
this.props.close() this.props.close()
}) })
} }
handleMarkdownNoteButtonKeyDown (e) { handleMarkdownNoteButtonKeyDown (e) {
if (e.keyCode === 9) { if (e.keyCode === 9) {
e.preventDefault() e.preventDefault()
@@ -50,8 +53,11 @@ class NewNoteModal extends React.Component {
handleSnippetNoteButtonClick (e) { handleSnippetNoteButtonClick (e) {
let { storage, folder, dispatch, location } = this.props let { storage, folder, dispatch, location } = this.props
dataApi dataApi
.createSnippetNote(storage, folder, { .createNote(storage, {
type: 'SNIPPET_NOTE',
folder: folder,
title: '', title: '',
description: '', description: '',
snippets: [{ snippets: [{
@@ -62,12 +68,12 @@ class NewNoteModal extends React.Component {
}) })
.then((note) => { .then((note) => {
dispatch({ dispatch({
type: 'CREATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
hashHistory.push({ hashHistory.push({
pathname: location.pathname, pathname: location.pathname,
query: {key: note.uniqueKey} query: {key: note.storage + '-' + note.key}
}) })
ee.emit('detail:focus') ee.emit('detail:focus')
this.props.close() this.props.close()

View File

@@ -7,8 +7,7 @@ import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store' import store from 'browser/main/store'
const electron = require('electron') const electron = require('electron')
const { shell, remote } = electron const { shell } = electron
const { Menu, MenuItem } = remote
import { SketchPicker } from 'react-color' import { SketchPicker } from 'react-color'
class UnstyledFolderItem extends React.Component { class UnstyledFolderItem extends React.Component {
@@ -42,10 +41,10 @@ class UnstyledFolderItem extends React.Component {
color: this.state.folder.color, color: this.state.folder.color,
name: this.state.folder.name name: this.state.folder.name
}) })
.then((storage) => { .then((data) => {
store.dispatch({ store.dispatch({
type: 'UPDATE_STORAGE', type: 'UPDATE_FOLDER',
storage: storage storage: data.storage
}) })
this.setState({ this.setState({
status: 'IDLE' status: 'IDLE'
@@ -55,10 +54,10 @@ class UnstyledFolderItem extends React.Component {
handleColorButtonClick (e) { handleColorButtonClick (e) {
const folder = Object.assign({}, this.state.folder, { showColumnPicker: true, colorPickerPos: { left: 0, top: 0 } }) const folder = Object.assign({}, this.state.folder, { showColumnPicker: true, colorPickerPos: { left: 0, top: 0 } })
this.setState({ folder }, function() { this.setState({ folder }, function () {
// After the color picker has been painted, re-calculate its position // After the color picker has been painted, re-calculate its position
// by comparing its dimensions to the host dimensions. // by comparing its dimensions to the host dimensions.
const { hostBoundingBox } = this.props; const { hostBoundingBox } = this.props
const colorPickerNode = ReactDOM.findDOMNode(this.refs.colorPicker) const colorPickerNode = ReactDOM.findDOMNode(this.refs.colorPicker)
const colorPickerBox = colorPickerNode.getBoundingClientRect() const colorPickerBox = colorPickerNode.getBoundingClientRect()
const offsetTop = hostBoundingBox.bottom - colorPickerBox.bottom const offsetTop = hostBoundingBox.bottom - colorPickerBox.bottom
@@ -103,19 +102,22 @@ class UnstyledFolderItem extends React.Component {
<button styleName='folderList-item-left-colorButton' style={{color: this.state.folder.color}} <button styleName='folderList-item-left-colorButton' style={{color: this.state.folder.color}}
onClick={(e) => !this.state.folder.showColumnPicker && this.handleColorButtonClick(e)} onClick={(e) => !this.state.folder.showColumnPicker && this.handleColorButtonClick(e)}
> >
{ this.state.folder.showColumnPicker ? {this.state.folder.showColumnPicker
<div style={ popover }> ? <div style={popover}>
<div style={ cover } <div style={cover}
onClick={ () => this.handleColorPickerClose() } /> onClick={() => this.handleColorPickerClose()}
/>
<div style={pickerStyle}> <div style={pickerStyle}>
<SketchPicker <SketchPicker
ref="colorPicker" ref='colorPicker'
color={this.state.folder.color} color={this.state.folder.color}
onChange={ (color) => this.handleColorChange(color) } onChange={(color) => this.handleColorChange(color)}
onChangeComplete={ (color) => this.handleColorChange(color) } /> onChangeComplete={(color) => this.handleColorChange(color)}
/>
</div> </div>
</div> </div>
: null } : null
}
<i className='fa fa-square'/> <i className='fa fa-square'/>
</button> </button>
<input styleName='folderList-item-left-nameInput' <input styleName='folderList-item-left-nameInput'
@@ -143,12 +145,12 @@ class UnstyledFolderItem extends React.Component {
handleDeleteConfirmButtonClick (e) { handleDeleteConfirmButtonClick (e) {
let { storage, folder } = this.props let { storage, folder } = this.props
dataApi dataApi
.removeFolder(storage.key, folder.key) .deleteFolder(storage.key, folder.key)
.then((storage) => { .then((data) => {
store.dispatch({ store.dispatch({
type: 'REMOVE_FOLDER', type: 'DELETE_FOLDER',
key: folder.key, storage: data.storage,
storage: storage folderKey: data.folderKey
}) })
}) })
} }
@@ -254,10 +256,10 @@ class StorageItem extends React.Component {
} }
dataApi.createFolder(storage.key, input) dataApi.createFolder(storage.key, input)
.then((storage) => { .then((data) => {
store.dispatch({ store.dispatch({
type: 'ADD_FOLDER', type: 'UPDATE_FOLDER',
storage: storage storage: data.storage
}) })
}) })
.catch((err) => { .catch((err) => {
@@ -276,11 +278,11 @@ class StorageItem extends React.Component {
.then(() => { .then(() => {
store.dispatch({ store.dispatch({
type: 'REMOVE_STORAGE', type: 'REMOVE_STORAGE',
key: storage.key storageKey: storage.key
}) })
}) })
.catch((err) => { .catch((err) => {
console.error(err) throw err
}) })
} }

View File

@@ -51,14 +51,13 @@ class StoragesTab extends React.Component {
} }
renderList () { renderList () {
let { storages, boundingBox } = this.props let { data, boundingBox } = this.props
if (!boundingBox) { return null } if (!boundingBox) { return null }
let storageList = storages.map((storage) => { let storageList = data.storageMap.map((storage) => {
return <StorageItem return <StorageItem
key={storage.key} key={storage.key}
storage={storage} storage={storage}
test={true}
hostBoundingBox={boundingBox} hostBoundingBox={boundingBox}
/> />
}) })

View File

@@ -33,8 +33,8 @@ class Preferences extends React.Component {
} }
renderContent () { renderContent () {
const { boundingBox } = this.state; const { boundingBox } = this.state
let { dispatch, config, storages } = this.props let { dispatch, config, data } = this.props
switch (this.state.currentTab) { switch (this.state.currentTab) {
case 'INFO': case 'INFO':
@@ -51,7 +51,7 @@ class Preferences extends React.Component {
return ( return (
<StoragesTab <StoragesTab
dispatch={dispatch} dispatch={dispatch}
storages={storages} data={data}
boundingBox={boundingBox} boundingBox={boundingBox}
/> />
) )
@@ -66,7 +66,7 @@ class Preferences extends React.Component {
getContentBoundingBox () { getContentBoundingBox () {
const node = ReactDOM.findDOMNode(this.refs.content) const node = ReactDOM.findDOMNode(this.refs.content)
return node.getBoundingClientRect(); return node.getBoundingClientRect()
} }
render () { render () {

View File

@@ -1,95 +1,449 @@
import { combineReducers, createStore } from 'redux' import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux' import { routerReducer } from 'react-router-redux'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import { Map, Set } from 'browser/lib/Mutable'
import _ from 'lodash'
function storages (state = [], action) { function defaultDataMap () {
console.info('REDUX >> ', action) return {
switch (action.type) { storageMap: new Map(),
case 'INIT_ALL': noteMap: new Map(),
return action.storages starredSet: new Set(),
case 'ADD_STORAGE': storageNoteMap: new Map(),
{ folderNoteMap: new Map(),
let storages = state.slice() tagNoteMap: new Map()
storages.push(action.storage)
return storages
} }
case 'ADD_FOLDER':
case 'REMOVE_FOLDER':
case 'UPDATE_STORAGE':
case 'RENAME_STORAGE':
{
let storages = state.slice()
storages = storages
.filter((storage) => storage.key !== action.storage.key)
storages.push(action.storage)
return storages
}
case 'REMOVE_STORAGE':
{
let storages = state.slice()
storages = storages
.filter((storage) => storage.key !== action.key)
return storages
}
}
return state
} }
function notes (state = [], action) { function data (state = defaultDataMap(), action) {
switch (action.type) { switch (action.type) {
case 'INIT_ALL': case 'INIT_ALL':
return action.notes state = defaultDataMap()
case 'ADD_STORAGE':
{
let notes = state.concat(action.notes)
return notes
}
case 'REMOVE_STORAGE':
{
let notes = state.slice()
notes = notes
.filter((note) => note.storage !== action.key)
return notes action.storages.forEach((storage) => {
} state.storageMap.set(storage.key, storage)
case 'REMOVE_FOLDER': })
{
let notes = state.slice()
notes = notes
.filter((note) => note.storage !== action.storage.key || note.folder !== action.key)
return notes action.notes.forEach((note) => {
let uniqueKey = note.storage + '-' + note.key
let folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note)
if (note.isStarred) {
state.starredSet.add(uniqueKey)
} }
case 'CREATE_NOTE':
{ let storageNoteList = state.storageNoteMap.get(note.storage)
let notes = state.slice() if (storageNoteList == null) {
notes.push(action.note) storageNoteList = new Set(storageNoteList)
return notes state.storageNoteMap.set(note.storage, storageNoteList)
} }
storageNoteList.add(uniqueKey)
let folderNoteSet = state.folderNoteMap.get(folderKey)
if (folderNoteSet == null) {
folderNoteSet = new Set(folderNoteSet)
state.folderNoteMap.set(folderKey, folderNoteSet)
}
folderNoteSet.add(uniqueKey)
note.tags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList == null) {
tagNoteList = new Set(tagNoteList)
state.tagNoteMap.set(tag, tagNoteList)
}
tagNoteList.add(uniqueKey)
})
})
return state
case 'UPDATE_NOTE': case 'UPDATE_NOTE':
{ {
let notes = state.slice() let note = action.note
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage) let uniqueKey = note.storage + '-' + note.key
notes.push(action.note) let folderKey = note.storage + '-' + note.folder
return notes let oldNote = state.noteMap.get(uniqueKey)
state = Object.assign({}, state)
state.noteMap = new Map(state.noteMap)
state.noteMap.set(uniqueKey, note)
if (oldNote == null || oldNote.isStarred !== note.isStarred) {
state.starredSet = new Set(state.starredSet)
if (note.isStarred) {
state.starredSet.add(uniqueKey)
} else {
state.starredSet.delete(uniqueKey)
}
}
// Update storageNoteMap if oldNote doesn't exist
if (oldNote == null) {
state.storageNoteMap = new Map(state.storageNoteMap)
let storageNoteSet = state.storageNoteMap.get(note.storage)
storageNoteSet = new Set(storageNoteSet)
storageNoteSet.add(uniqueKey)
state.storageNoteMap.set(note.storage, storageNoteSet)
}
// Update foldermap if folder changed or post created
if (oldNote == null || oldNote.folder !== note.folder) {
state.folderNoteMap = new Map(state.folderNoteMap)
let folderNoteSet = state.folderNoteMap.get(folderKey)
folderNoteSet = new Set(folderNoteSet)
folderNoteSet.add(uniqueKey)
state.folderNoteMap.set(folderKey, folderNoteSet)
if (oldNote != null) {
let oldFolderKey = oldNote.storage + '-' + oldNote.folder
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
oldFolderNoteList = new Set(oldFolderNoteList)
oldFolderNoteList.delete(uniqueKey)
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
}
}
if (oldNote != null) {
let discardedTags = _.difference(oldNote.tags, note.tags)
let addedTags = _.difference(note.tags, oldNote.tags)
if (discardedTags.length + addedTags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
discardedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList != null) {
tagNoteList = new Set(tagNoteList)
tagNoteList.delete(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
}
})
addedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
tagNoteList = new Set(tagNoteList)
tagNoteList.add(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
})
}
} else {
state.tagNoteMap = new Map(state.tagNoteMap)
note.tags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList == null) {
tagNoteList = new Set(tagNoteList)
state.tagNoteMap.set(tag, tagNoteList)
}
tagNoteList.add(uniqueKey)
})
}
return state
} }
case 'MOVE_NOTE': case 'MOVE_NOTE':
{ {
let notes = state.slice() let originNote = action.originNote
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage) let originKey = originNote.storage + '-' + originNote.key
notes.push(action.newNote) let note = action.note
return notes let uniqueKey = note.storage + '-' + note.key
let folderKey = note.storage + '-' + note.folder
let oldNote = state.noteMap.get(uniqueKey)
state = Object.assign({}, state)
state.noteMap = new Map(state.noteMap)
state.noteMap.delete(originKey)
state.noteMap.set(uniqueKey, note)
// If storage chanced, origin key must be discarded
if (originKey !== uniqueKey) {
console.log('diffrent storage')
// From isStarred
if (originNote.isStarred) {
state.starredSet = new Set(state.starredSet)
state.starredSet.delete(originKey)
} }
case 'REMOVE_NOTE':
// From storageNoteMap
state.storageNoteMap = new Map(state.storageNoteMap)
let noteSet = state.storageNoteMap.get(originNote.storage)
noteSet = new Set(noteSet)
noteSet.delete(originKey)
state.storageNoteMap.set(originNote.storage, noteSet)
// From folderNoteMap
state.folderNoteMap = new Map(state.folderNoteMap)
let originFolderKey = originNote.storage + '-' + originNote.folder
let originFolderList = state.folderNoteMap.get(originFolderKey)
originFolderList = new Set(originFolderList)
originFolderList.delete(originKey)
state.folderNoteMap.set(originFolderKey, originFolderList)
// From tagMap
if (originNote.tags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
originNote.tags.forEach((tag) => {
let noteSet = state.tagNoteMap.get(tag)
noteSet = new Set(noteSet)
noteSet.delete(originKey)
state.tagNoteMap.set(tag, noteSet)
})
}
}
if (oldNote == null || oldNote.isStarred !== note.isStarred) {
state.starredSet = new Set(state.starredSet)
if (note.isStarred) {
state.starredSet.add(uniqueKey)
} else {
state.starredSet.delete(uniqueKey)
}
}
// Update storageNoteMap if oldNote doesn't exist
if (oldNote == null) {
state.storageNoteMap = new Map(state.storageNoteMap)
let noteSet = state.storageNoteMap.get(note.storage)
noteSet = new Set(noteSet)
noteSet.add(uniqueKey)
state.folderNoteMap.set(folderKey, noteSet)
}
// Update foldermap if folder changed or post created
if (oldNote == null || oldNote.folder !== note.folder) {
state.folderNoteMap = new Map(state.folderNoteMap)
let folderNoteList = state.folderNoteMap.get(folderKey)
folderNoteList = new Set(folderNoteList)
folderNoteList.add(uniqueKey)
state.folderNoteMap.set(folderKey, folderNoteList)
if (oldNote != null) {
let oldFolderKey = oldNote.storage + '-' + oldNote.folder
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
oldFolderNoteList = new Set(oldFolderNoteList)
oldFolderNoteList.delete(uniqueKey)
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
}
}
// Remove from old folder map
if (oldNote != null) {
let discardedTags = _.difference(oldNote.tags, note.tags)
let addedTags = _.difference(note.tags, oldNote.tags)
if (discardedTags.length + addedTags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
discardedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList != null) {
tagNoteList = new Set(tagNoteList)
tagNoteList.delete(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
}
})
addedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
tagNoteList = new Set(tagNoteList)
tagNoteList.add(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
})
}
} else {
state.tagNoteMap = new Map(state.tagNoteMap)
note.tags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList == null) {
tagNoteList = new Set(tagNoteList)
state.tagNoteMap.set(tag, tagNoteList)
}
tagNoteList.add(uniqueKey)
})
}
return state
}
case 'DELETE_NOTE':
{ {
let notes = state.slice() let uniqueKey = action.storageKey + '-' + action.noteKey
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage) let targetNote = state.noteMap.get(uniqueKey)
return notes
state = Object.assign({}, state)
// From storageNoteMap
state.storageNoteMap = new Map(state.storageNoteMap)
let noteSet = state.storageNoteMap.get(targetNote.storage)
noteSet = new Set(noteSet)
noteSet.delete(uniqueKey)
state.storageNoteMap.set(targetNote.storage, noteSet)
if (targetNote != null) {
// From isStarred
if (targetNote.isStarred) {
state.starredSet = new Set(state.starredSet)
state.starredSet.delete(uniqueKey)
} }
// From folderNoteMap
let folderKey = targetNote.storage + '-' + targetNote.folder
state.folderNoteMap = new Map(state.folderNoteMap)
let folderSet = state.folderNoteMap.get(folderKey)
folderSet = new Set(folderSet)
folderSet.delete(uniqueKey)
state.folderNoteMap.set(folderKey, folderSet)
// From tagMap
if (targetNote.tags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
targetNote.tags.forEach((tag) => {
let noteSet = state.tagNoteMap.get(tag)
noteSet = new Set(noteSet)
noteSet.delete(uniqueKey)
state.tagNoteMap.set(tag, noteSet)
})
}
}
state.noteMap = new Map(state.noteMap)
state.noteMap.delete(uniqueKey)
return state
}
case 'UPDATE_FOLDER':
{
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
}
return state
case 'DELETE_FOLDER':
{
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
// Get note list from folder-note map
// and delete note set from folder-note map
let folderKey = action.storage.key + '-' + action.folderKey
let noteSet = state.folderNoteMap.get(folderKey)
state.folderNoteMap = new Map(state.folderNoteMap)
state.folderNoteMap.delete(folderKey)
state.noteMap = new Map(state.noteMap)
state.storageNoteMap = new Map(state.storageNoteMap)
let storageNoteSet = state.storageNoteMap.get(action.storage.key)
storageNoteSet = new Set(storageNoteSet)
state.storageNoteMap.set(action.storage.key, storageNoteSet)
noteSet.forEach(function handleNoteKey (noteKey) {
// Get note from noteMap
let note = state.noteMap.get(noteKey)
if (note != null) {
state.noteMap.delete(noteKey)
// From storageSet
storageNoteSet.delete(noteKey)
// From starredSet
if (note.isStarred) {
state.starredSet = new Set(state.starredSet)
state.starredSet.delete(noteKey)
}
// Delete key from tag map
state.tagNoteMap = new Map(state.tagNoteMap)
note.tags.forEach((tag) => {
let tagNoteSet = state.tagNoteMap.get(tag)
tagNoteSet = new Set(tagNoteSet)
state.tagNoteMap.set(tag, tagNoteSet)
tagNoteSet.delete(noteKey)
})
}
})
}
return state
case 'ADD_STORAGE':
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
state.noteMap = new Map(state.noteMap)
state.storageNoteMap = new Map(state.storageNoteMap)
state.storageNoteMap.set(action.storage.key, new Set())
state.folderNoteMap = new Map(state.folderNoteMap)
state.tagNoteMap = new Map(state.tagNoteMap)
action.notes.forEach((note) => {
let uniqueKey = note.storage + '-' + note.key
let folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note)
if (note.isStarred) {
state.starredSet.add(uniqueKey)
}
let storageNoteList = state.storageNoteMap.get(note.storage)
if (storageNoteList == null) {
storageNoteList = new Set(storageNoteList)
state.storageNoteMap.set(note.storage, storageNoteList)
}
storageNoteList.add(uniqueKey)
let folderNoteSet = state.folderNoteMap.get(folderKey)
if (folderNoteSet == null) {
folderNoteSet = new Set(folderNoteSet)
state.folderNoteMap.set(folderKey, folderNoteSet)
}
folderNoteSet.add(uniqueKey)
note.tags.forEach((tag) => {
let tagNoteSet = state.tagNoteMap.get(tag)
if (tagNoteSet == null) {
tagNoteSet = new Set(tagNoteSet)
state.tagNoteMap.set(tag, tagNoteSet)
}
tagNoteSet.add(uniqueKey)
})
})
return state
case 'REMOVE_STORAGE':
state = Object.assign({}, state)
let storage = state.storageMap.get(action.storageKey)
state.storageMap = new Map(state.storageMap)
state.storageMap.delete(action.storageKey)
// Remove folders from folderMap
if (storage != null) {
state.folderMap = new Map(state.folderMap)
storage.folders.forEach((folder) => {
let folderKey = storage.key + '-' + folder.key
state.folderMap.delete(folderKey)
})
}
// Remove notes from noteMap and tagNoteMap
let storageNoteSet = state.storageNoteMap.get(action.storageKey)
state.storageNoteMap = new Map(state.storageNoteMap)
state.storageNoteMap.delete(action.storageKey)
if (storageNoteSet != null) {
let notes = storageNoteSet
.map((noteKey) => state.noteMap.get(noteKey))
.filter((note) => note != null)
state.noteMap = new Map(state.noteMap)
state.tagNoteMap = new Map(state.tagNoteMap)
state.starredSet = new Set(state.starredSet)
notes.forEach((note) => {
let noteKey = storage.key + '-' + note.key
state.noteMap.delete(noteKey)
state.starredSet.delete(noteKey)
note.tags.forEach((tag) => {
let tagNoteSet = state.tagNoteMap.get(tag)
tagNoteSet = new Set(tagNoteSet)
tagNoteSet.delete(noteKey)
})
})
}
return state
case 'RENAME_STORAGE':
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
return state
} }
return state return state
} }
@@ -116,8 +470,7 @@ function config (state = defaultConfig, action) {
} }
let reducer = combineReducers({ let reducer = combineReducers({
storages, data,
notes,
config, config,
routing: routerReducer routing: routerReducer
}) })

View File

@@ -48,6 +48,7 @@
"electron-gh-releases": "^2.0.2", "electron-gh-releases": "^2.0.2",
"font-awesome": "^4.3.0", "font-awesome": "^4.3.0",
"highlight.js": "^9.3.0", "highlight.js": "^9.3.0",
"immutable": "^3.8.1",
"lodash": "^4.11.1", "lodash": "^4.11.1",
"markdown-it": "^6.0.1", "markdown-it": "^6.0.1",
"markdown-it-checkbox": "^1.1.0", "markdown-it-checkbox": "^1.1.0",
@@ -76,6 +77,7 @@
"dom-storage": "^2.0.2", "dom-storage": "^2.0.2",
"electron-packager": "^6.0.0", "electron-packager": "^6.0.0",
"electron-prebuilt": "^1.2.8", "electron-prebuilt": "^1.2.8",
"faker": "^3.1.0",
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-electron-installer": "^1.2.0", "grunt-electron-installer": "^1.2.0",
"history": "^1.17.0", "history": "^1.17.0",

View File

@@ -8,86 +8,65 @@ global.navigator = window.navigator
const Storage = require('dom-storage') const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true }) const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path') const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander') const sander = require('sander')
const _ = require('lodash') const _ = require('lodash')
const os = require('os')
const CSON = require('season')
function copyFile (filePath, targetPath) { const v1StoragePath = path.join(os.tmpdir(), 'test/addStorage-v1-storage')
return sander.readFile(filePath) // const legacyStoragePath = path.join(os.tmpdir(), 'test/addStorage-legacy-storage')
.then(function writeFile (data) { // const emptyDirPath = path.join(os.tmpdir(), 'test/addStorage-empty-storage')
return sander.writeFile(targetPath, data.toString())
})
}
test('add a initialized storage', (t) => { test.beforeEach((t) => {
const dummyStoragePath = path.join(__dirname, '../dummy/dummyStorage') t.context.v1StorageData = TestDummy.dummyStorage(v1StoragePath)
const targetPath = path.join(__dirname, '../sandbox/test-add-storage1') // t.context.legacyStorageData = TestDummy.dummyLegacyStorage(legacyStoragePath)
localStorage.setItem('storages', JSON.stringify([]))
})
test.serial('Add Storage', (t) => {
const input = { const input = {
type: 'FILESYSTEM', type: 'FILESYSTEM',
name: 'test-add-storage1', name: 'add-storage1',
path: targetPath path: v1StoragePath
} }
return Promise.resolve() return Promise.resolve()
.then(function before () { .then(function doTest () {
localStorage.setItem('storages', JSON.stringify([]))
sander.rimrafSync(targetPath)
return copyFile(path.join(dummyStoragePath, 'boostnote.json'), path.join(targetPath, 'boostnote.json'))
.then(() => {
return copyFile(path.join(dummyStoragePath, 'fc6ba88e8ecf/data.json'), path.join(targetPath, 'fc6ba88e8ecf/data.json'))
})
})
.then(function doTest (data) {
return addStorage(input) return addStorage(input)
}) })
.then(function validateResult (data) { .then(function validateResult (data) {
const { storage, notes } = data let { storage, notes } = data
// Check data.storage
t.true(_.isString(storage.key)) t.true(_.isString(storage.key))
t.is(storage.name, 'test-add-storage1') t.is(storage.name, input.name)
t.true(_.isArray(storage.folders)) t.is(storage.type, input.type)
t.is(storage.folders.length, 1) t.is(storage.path, input.path)
t.true(_.isArray(notes)) t.is(storage.version, '1.0')
t.is(notes.length, 2) t.is(storage.folders.length, t.context.v1StorageData.json.folders.length)
t.is(notes[0].folder, 'fc6ba88e8ecf')
t.is(notes[0].storage, storage.key) // Check data.notes
t.is(notes.length, t.context.v1StorageData.notes.length)
notes.forEach(function validateNote (note) {
t.is(note.storage, storage.key)
}) })
.then(function after () {
localStorage.clear() // Check localStorage
sander.rimrafSync(targetPath) let cacheData = _.find(JSON.parse(localStorage.getItem('storages')), {key: data.storage.key})
t.is(cacheData.name, input.name)
t.is(cacheData.type, input.type)
t.is(cacheData.path, input.path)
// Check boostnote.json
let jsonData = CSON.readFileSync(path.join(storage.path, 'boostnote.json'))
t.true(_.isArray(jsonData.folders))
t.is(jsonData.version, '1.0')
t.is(jsonData.folders.length, t.context.v1StorageData.json.folders.length)
}) })
}) })
test('add a fresh storage', (t) => { test.after.always(() => {
const targetPath = path.join(__dirname, '../sandbox/test-add-storage2')
const input = {
type: 'FILESYSTEM',
name: 'test-add-storage2',
path: targetPath
}
return Promise.resolve()
.then(function before () {
localStorage.setItem('storages', JSON.stringify([]))
sander.rimrafSync(targetPath)
})
.then(function doTest (data) {
return addStorage(input)
})
.then(function validateResult (data) {
const { storage, notes } = data
t.true(_.isString(storage.key))
t.is(storage.name, 'test-add-storage2')
t.true(_.isArray(storage.folders))
t.is(storage.folders.length, 0)
t.true(_.isArray(notes))
t.is(notes.length, 0)
t.true(sander.statSync(path.join(targetPath, 'boostnote.json')).isFile())
})
.then(function after () {
localStorage.clear() localStorage.clear()
sander.rimrafSync(targetPath) sander.rimrafSync(v1StoragePath)
})
}) })

View File

@@ -0,0 +1,45 @@
const test = require('ava')
const createFolder = require('browser/main/lib/dataApi/createFolder')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const _ = require('lodash')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('season')
const storagePath = path.join(os.tmpdir(), 'test/create-folder')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Create a folder', (t) => {
const storageKey = t.context.storage.cache.key
const input = {
name: 'created',
color: '#ff5555'
}
return Promise.resolve()
.then(function doTest () {
return createFolder(storageKey, input)
})
.then(function assert (data) {
t.true(_.find(data.storage.folders, input) != null)
let jsonData = CSON.readFileSync(path.join(data.storage.path, 'boostnote.json'))
console.log(path.join(data.storage.path, 'boostnote.json'))
t.true(_.find(jsonData.folders, input) != null)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -0,0 +1,89 @@
const test = require('ava')
const createNote = require('browser/main/lib/dataApi/createNote')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('season')
const faker = require('faker')
const storagePath = path.join(os.tmpdir(), 'test/create-note')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Create a note', (t) => {
const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key
const input1 = {
type: 'SNIPPET_NOTE',
description: faker.lorem.lines(),
snippets: [{
name: faker.system.fileName(),
mode: 'text',
content: faker.lorem.lines()
}],
tags: faker.lorem.words().split(' '),
folder: folderKey
}
input1.title = input1.description.split('\n').shift()
const input2 = {
type: 'MARKDOWN_NOTE',
content: faker.lorem.lines(),
tags: faker.lorem.words().split(' '),
folder: folderKey
}
input2.title = input2.content.split('\n').shift()
return Promise.resolve()
.then(function doTest () {
return Promise.all([
createNote(storageKey, input1),
createNote(storageKey, input2)
])
})
.then(function assert (data) {
let data1 = data[0]
let data2 = data[1]
t.is(storageKey, data1.storage)
let jsonData1 = CSON.readFileSync(path.join(storagePath, 'notes', data1.key + '.cson'))
t.is(input1.title, data1.title)
t.is(input1.title, jsonData1.title)
t.is(input1.description, data1.description)
t.is(input1.description, jsonData1.description)
t.is(input1.tags.length, data1.tags.length)
t.is(input1.tags.length, jsonData1.tags.length)
t.is(input1.snippets.length, data1.snippets.length)
t.is(input1.snippets.length, jsonData1.snippets.length)
t.is(input1.snippets[0].content, data1.snippets[0].content)
t.is(input1.snippets[0].content, jsonData1.snippets[0].content)
t.is(input1.snippets[0].name, data1.snippets[0].name)
t.is(input1.snippets[0].name, jsonData1.snippets[0].name)
t.is(storageKey, data2.storage)
let jsonData2 = CSON.readFileSync(path.join(storagePath, 'notes', data2.key + '.cson'))
t.is(input2.title, data2.title)
t.is(input2.title, jsonData2.title)
t.is(input2.content, data2.content)
t.is(input2.content, jsonData2.content)
t.is(input2.tags.length, data2.tags.length)
t.is(input2.tags.length, jsonData2.tags.length)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -0,0 +1,45 @@
const test = require('ava')
const deleteFolder = require('browser/main/lib/dataApi/deleteFolder')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const _ = require('lodash')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('season')
const storagePath = path.join(os.tmpdir(), 'test/delete-folder')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Delete a folder', (t) => {
const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key
return Promise.resolve()
.then(function doTest () {
return deleteFolder(storageKey, folderKey)
})
.then(function assert (data) {
t.true(_.find(data.storage.folders, {key: folderKey}) == null)
let jsonData = CSON.readFileSync(path.join(data.storage.path, 'boostnote.json'))
t.true(_.find(jsonData.folders, {key: folderKey}) == null)
let notePaths = sander.readdirSync(data.storage.path, 'notes')
t.is(notePaths.length, t.context.storage.notes.filter((note) => note.folder !== folderKey).length)
})
})
test.after.always(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -0,0 +1,62 @@
const test = require('ava')
const createNote = require('browser/main/lib/dataApi/createNote')
const deleteNote = require('browser/main/lib/dataApi/deleteNote')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('season')
const faker = require('faker')
const storagePath = path.join(os.tmpdir(), 'test/delete-note')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Delete a note', (t) => {
const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key
const input1 = {
type: 'SNIPPET_NOTE',
description: faker.lorem.lines(),
snippets: [{
name: faker.system.fileName(),
mode: 'text',
content: faker.lorem.lines()
}],
tags: faker.lorem.words().split(' '),
folder: folderKey
}
input1.title = input1.description.split('\n').shift()
return Promise.resolve()
.then(function doTest () {
return createNote(storageKey, input1)
.then(function (data) {
return deleteNote(storageKey, data.key)
})
})
.then(function assert (data) {
try {
CSON.readFileSync(path.join(storagePath, 'notes', data.noteKey + '.cson'))
t.fail('note cson must be deleted.')
} catch (err) {
t.is(err.code, 'ENOENT')
}
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -8,68 +8,58 @@ global.navigator = window.navigator
const Storage = require('dom-storage') const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true }) const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path') const path = require('path')
const crypto = require('crypto') const TestDummy = require('../fixtures/TestDummy')
const keygen = require('browser/lib/keygen')
const sander = require('sander')
const _ = require('lodash')
const os = require('os')
test.serial('Fetch storages and notes', (t) => { const v1StoragePath = path.join(os.tmpdir(), 'test/init-v1-storage')
const dummyStoragePath = path.join(__dirname, '..', 'dummy/dummyStorage') const legacyStoragePath = path.join(os.tmpdir(), 'test/init-legacy-storage')
const dummyRawStorage = { const emptyDirPath = path.join(os.tmpdir(), 'test/init-empty-storage')
name: 'test1',
key: crypto.randomBytes(6).toString('hex'), test.beforeEach((t) => {
path: dummyStoragePath localStorage.clear()
// Prepare 3 types of dir
t.context.v1StorageData = TestDummy.dummyStorage(v1StoragePath, {cache: {name: 'v1'}})
t.context.legacyStorageData = TestDummy.dummyLegacyStorage(legacyStoragePath, {cache: {name: 'legacy'}})
t.context.emptyStorageData = {
cache: {
type: 'FILESYSTEM',
name: 'empty',
key: keygen(),
path: emptyDirPath
}
} }
const dummyFolderKey = 'fc6ba88e8ecf'
localStorage.setItem('storages', JSON.stringify([t.context.v1StorageData.cache, t.context.legacyStorageData.cache, t.context.emptyStorageData.cache]))
})
test.serial('Initialize All Storages', (t) => {
const { v1StorageData, legacyStorageData, emptyStorageData } = t.context
return Promise.resolve() return Promise.resolve()
.then(function before () {
localStorage.setItem('storages', JSON.stringify([dummyRawStorage]))
})
.then(function test () { .then(function test () {
return init() return init()
}) })
.then(function assert (data) { .then(function assert (data) {
t.true(Array.isArray(data.storages)) t.true(Array.isArray(data.storages))
var targetStorage = data.storages.filter((storage) => storage.key === dummyRawStorage.key)[0] t.is(data.notes.length, v1StorageData.notes.length + legacyStorageData.notes.length)
t.not(targetStorage, null) t.is(data.storages.length, 3)
t.is(targetStorage.name, dummyRawStorage.name) data.storages.forEach(function assertStorage (storage) {
t.is(targetStorage.key, dummyRawStorage.key) t.true(_.isString(storage.key))
t.is(targetStorage.path, dummyRawStorage.path) t.true(_.isString(storage.name))
t.is(data.notes.length, 2) t.true(storage.type === 'FILESYSTEM')
data.notes.forEach((note) => { t.true(_.isString(storage.path))
t.is(note.folder, dummyFolderKey)
}) })
t.true(Array.isArray(data.notes))
}) })
.then(function after () { .then(function after () {
localStorage.clear() localStorage.clear()
}) })
}) })
test.serial('If storage path is a empty folder, return metadata with empty folder array and empty note array.', (t) => { test.after.always(() => {
const emptyFolderPath = path.join(__dirname, '..', 'dummy/empty')
const dummyRawStorage = {
name: 'test2',
key: crypto.randomBytes(6).toString('hex'),
path: emptyFolderPath
}
return Promise.resolve()
.then(function before () {
localStorage.setItem('storages', JSON.stringify([dummyRawStorage]))
})
.then(function test () {
return init()
})
.then(function assert (data) {
t.true(Array.isArray(data.storages))
var targetStorage = data.storages.filter((storage) => storage.key === dummyRawStorage.key)[0]
t.not(targetStorage, null)
t.is(targetStorage.name, dummyRawStorage.name)
t.is(targetStorage.key, dummyRawStorage.key)
t.is(targetStorage.path, dummyRawStorage.path)
t.true(Array.isArray(data.notes))
})
.then(function after () {
localStorage.clear() localStorage.clear()
}) sander.rimrafSync(v1StoragePath)
sander.rimrafSync(legacyStoragePath)
sander.rimrafSync(emptyDirPath)
}) })

View File

@@ -0,0 +1,64 @@
const test = require('ava')
const migrateFromV6Storage = require('browser/main/lib/dataApi/migrateFromV6Storage')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const CSON = require('season')
const _ = require('lodash')
const os = require('os')
const dummyStoragePath = path.join(os.tmpdir(), 'test/migrate-test-storage')
test.beforeEach((t) => {
let dummyData = t.context.dummyData = TestDummy.dummyLegacyStorage(dummyStoragePath)
console.log('init count', dummyData.notes.length)
localStorage.setItem('storages', JSON.stringify([dummyData.cache]))
})
test.serial('Migrate legacy storage into v1 storage', (t) => {
return Promise.resolve()
.then(function test () {
return migrateFromV6Storage(dummyStoragePath)
})
.then(function assert (data) {
// Check the result. It must be true if succeed.
t.true(data)
// Check all notes migrated.
let dummyData = t.context.dummyData
let noteDirPath = path.join(dummyStoragePath, 'notes')
let fileList = sander.readdirSync(noteDirPath)
t.is(dummyData.notes.length, fileList.length)
let noteMap = fileList
.map((filePath) => {
return CSON.readFileSync(path.join(noteDirPath, filePath))
})
dummyData.notes
.forEach(function (targetNote) {
t.true(_.find(noteMap, {title: targetNote.title, folder: targetNote.folder}) != null)
})
// Check legacy folder directory is removed
dummyData.json.folders
.forEach(function (folder) {
try {
sander.statSync(dummyStoragePath, folder.key)
t.fail('Folder still remains. ENOENT error must be occured.')
} catch (err) {
t.is(err.code, 'ENOENT')
}
})
})
})
test.after.always(function () {
localStorage.clear()
sander.rimrafSync(dummyStoragePath)
})

66
tests/dataApi/moveNote.js Normal file
View File

@@ -0,0 +1,66 @@
const test = require('ava')
const moveNote = require('browser/main/lib/dataApi/moveNote')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('season')
const faker = require('faker')
const storagePath = path.join(os.tmpdir(), 'test/move-note')
const storagePath2 = path.join(os.tmpdir(), 'test/move-note2')
test.beforeEach((t) => {
t.context.storage1 = TestDummy.dummyStorage(storagePath)
t.context.storage2 = TestDummy.dummyStorage(storagePath2)
localStorage.setItem('storages', JSON.stringify([t.context.storage1.cache, t.context.storage2.cache]))
})
test.serial('Move a note', (t) => {
const storageKey1 = t.context.storage1.cache.key
const folderKey1 = t.context.storage1.json.folders[0].key
const note1 = t.context.storage1.notes[0]
const note2 = t.context.storage1.notes[1]
const storageKey2 = t.context.storage2.cache.key
const folderKey2 = t.context.storage2.json.folders[0].key
return Promise.resolve()
.then(function doTest () {
return Promise.all([
moveNote(storageKey1, note1.key, storageKey1, folderKey1),
moveNote(storageKey1, note2.key, storageKey2, folderKey2)
])
})
.then(function assert (data) {
let data1 = data[0]
let data2 = data[1]
let jsonData1 = CSON.readFileSync(path.join(storagePath, 'notes', data1.key + '.cson'))
t.is(jsonData1.folder, folderKey1)
t.is(jsonData1.title, note1.title)
let jsonData2 = CSON.readFileSync(path.join(storagePath2, 'notes', data2.key + '.cson'))
t.is(jsonData2.folder, folderKey2)
t.is(jsonData2.title, note2.title)
try {
CSON.readFileSync(path.join(storagePath, 'notes', note2.key + '.cson'))
t.fail('The old note should be deleted.')
} catch (err) {
t.is(err.code, 'ENOENT')
}
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
sander.rimrafSync(storagePath2)
})

View File

@@ -0,0 +1,36 @@
const test = require('ava')
const removeStorage = require('browser/main/lib/dataApi/removeStorage')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const storagePath = path.join(os.tmpdir(), 'test/remove-storage')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test('Remove a storage', (t) => {
const storageKey = t.context.storage.cache.key
return Promise.resolve()
.then(function doTest () {
return removeStorage(storageKey)
})
.then(function assert (data) {
t.is(JSON.parse(localStorage.getItem('storages')).length, 0)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -0,0 +1,38 @@
const test = require('ava')
const renameStorage = require('browser/main/lib/dataApi/renameStorage')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const _ = require('lodash')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const storagePath = path.join(os.tmpdir(), 'test/rename-storage')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Rename a storage', (t) => {
const storageKey = t.context.storage.cache.key
return Promise.resolve()
.then(function doTest () {
return renameStorage(storageKey, 'changed')
})
.then(function assert (data) {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
t.true(_.find(cachedStorageList, {key: storageKey}).name === 'changed')
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -0,0 +1,46 @@
const test = require('ava')
const updateFolder = require('browser/main/lib/dataApi/updateFolder')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const _ = require('lodash')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('season')
const storagePath = path.join(os.tmpdir(), 'test/update-folder')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Update a folder', (t) => {
const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key
const input = {
name: 'changed',
color: '#FF0000'
}
return Promise.resolve()
.then(function doTest () {
return updateFolder(storageKey, folderKey, input)
})
.then(function assert (data) {
t.true(_.find(data.storage.folders, input) != null)
let jsonData = CSON.readFileSync(path.join(data.storage.path, 'boostnote.json'))
console.log(path.join(data.storage.path, 'boostnote.json'))
t.true(_.find(jsonData.folders, input) != null)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

116
tests/dataApi/updateNote.js Normal file
View File

@@ -0,0 +1,116 @@
const test = require('ava')
const createNote = require('browser/main/lib/dataApi/createNote')
const updateNote = require('browser/main/lib/dataApi/updateNote')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('season')
const faker = require('faker')
const storagePath = path.join(os.tmpdir(), 'test/update-note')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Update a note', (t) => {
const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key
const input1 = {
type: 'SNIPPET_NOTE',
description: faker.lorem.lines(),
snippets: [{
name: faker.system.fileName(),
mode: 'text',
content: faker.lorem.lines()
}],
tags: faker.lorem.words().split(' '),
folder: folderKey
}
input1.title = input1.description.split('\n').shift()
const input2 = {
type: 'MARKDOWN_NOTE',
content: faker.lorem.lines(),
tags: faker.lorem.words().split(' '),
folder: folderKey
}
input2.title = input2.content.split('\n').shift()
const input3 = {
type: 'SNIPPET_NOTE',
description: faker.lorem.lines(),
snippets: [{
name: faker.system.fileName(),
mode: 'text',
content: faker.lorem.lines()
}],
tags: faker.lorem.words().split(' ')
}
input3.title = input3.description.split('\n').shift()
const input4 = {
type: 'MARKDOWN_NOTE',
content: faker.lorem.lines(),
tags: faker.lorem.words().split(' ')
}
input4.title = input4.content.split('\n').shift()
return Promise.resolve()
.then(function doTest () {
return Promise
.all([
createNote(storageKey, input1),
createNote(storageKey, input2)
])
.then(function updateNotes (data) {
let data1 = data[0]
let data2 = data[1]
return Promise.all([
updateNote(data1.storage, data1.key, input3),
updateNote(data1.storage, data2.key, input4)
])
})
})
.then(function assert (data) {
let data1 = data[0]
let data2 = data[1]
let jsonData1 = CSON.readFileSync(path.join(storagePath, 'notes', data1.key + '.cson'))
t.is(input3.title, data1.title)
t.is(input3.title, jsonData1.title)
t.is(input3.description, data1.description)
t.is(input3.description, jsonData1.description)
t.is(input3.tags.length, data1.tags.length)
t.is(input3.tags.length, jsonData1.tags.length)
t.is(input3.snippets.length, data1.snippets.length)
t.is(input3.snippets.length, jsonData1.snippets.length)
t.is(input3.snippets[0].content, data1.snippets[0].content)
t.is(input3.snippets[0].content, jsonData1.snippets[0].content)
t.is(input3.snippets[0].name, data1.snippets[0].name)
t.is(input3.snippets[0].name, jsonData1.snippets[0].name)
let jsonData2 = CSON.readFileSync(path.join(storagePath, 'notes', data2.key + '.cson'))
t.is(input4.title, data2.title)
t.is(input4.title, jsonData2.title)
t.is(input4.content, data2.content)
t.is(input4.content, jsonData2.content)
t.is(input4.tags.length, data2.tags.length)
t.is(input4.tags.length, jsonData2.tags.length)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -1,9 +0,0 @@
{
"folders": [
{
"key": "fc6ba88e8ecf",
"name": "test",
"color": "#FF5555"
}
]
}

View File

@@ -1,24 +0,0 @@
{
"notes": [
{
"tags": [],
"title": "Footnote test",
"content": "# Footnote test\n\ntest test",
"type": "MARKDOWN_NOTE",
"key": "93c6ac2a7953",
"isStarred": false,
"createdAt": "2016-07-25T16:19:55.620Z",
"updatedAt": "2016-07-26T08:00:11.326Z"
},
{
"tags": [],
"title": "Checkbox test",
"content": "# Checkbox test\n\n- [x] Task1\n- [ ] Task2\n- [ ] Task3\n\n",
"type": "MARKDOWN_NOTE",
"key": "4568d84331d9",
"isStarred": false,
"createdAt": "2016-07-25T16:58:43.685Z",
"updatedAt": "2016-08-21T06:14:50.381Z"
}
]
}

179
tests/fixtures/TestDummy.js vendored Normal file
View File

@@ -0,0 +1,179 @@
const faker = require('faker')
const keygen = require('browser/lib/keygen')
const _ = require('lodash')
const sander = require('sander')
const CSON = require('season')
const path = require('path')
function dummyFolder (override = {}) {
var data = {
name: faker.lorem.word(),
color: faker.internet.color()
}
if (override.key == null) data.key = keygen()
Object.assign(data, override)
return data
}
function dummyBoostnoteJSONData (override = {}, isLegacy = false) {
var data = {}
if (override.folders == null) {
data.folders = []
var folderCount = Math.floor((Math.random() * 5)) + 1
for (var i = 0; i < folderCount; i++) {
var key = keygen()
while (data.folders.some((folder) => folder.key === key)) {
key = keygen()
}
data.folders.push(dummyFolder({
key
}))
}
}
if (!isLegacy) data.version = '1.0'
Object.assign(data, override)
return data
}
function dummyNote (override = {}) {
var data = Math.random() > 0.5
? {
type: 'MARKDOWN_NOTE',
content: faker.lorem.lines()
}
: {
type: 'SNIPPET_NOTE',
description: faker.lorem.lines(),
snippets: [{
name: faker.system.fileName(),
mode: 'text',
content: faker.lorem.lines()
}]
}
data.title = data.type === 'MARKDOWN_NOTE'
? data.content.split('\n').shift()
: data.description.split('\n').shift()
data.createdAt = faker.date.past()
data.updatedAt = faker.date.recent()
data.isStarred = false
data.tags = faker.lorem.words().split(' ')
if (override.key == null) data.key = keygen()
if (override.folder == null) data.folder = keygen()
Object.assign(data, override)
return data
}
/**
* @param {String}
* @param {Object}
* ```
* {
* json: {
* folders: []
* version: String(enum:'1.0')
* },
* cache: {
* key: String,
* name: String,
* type: String(enum:'FILESYSTEM'),
* path: String
* },
* notes: []
* }
* ```
* @return {[type]}
*/
function dummyStorage (storagePath, override = {}) {
var jsonData = override.json != null
? override.json
: dummyBoostnoteJSONData()
var cacheData = override.cache != null
? override.cache
: {}
if (cacheData.key == null) cacheData.key = keygen()
if (cacheData.name == null) cacheData.name = faker.random.word()
if (cacheData.type == null) cacheData.type = 'FILESYSTEM'
cacheData.path = storagePath
sander.writeFileSync(path.join(storagePath, 'boostnote.json'), JSON.stringify(jsonData))
var notesData = []
var noteCount = Math.floor((Math.random() * 15)) + 1
for (var i = 0; i < noteCount; i++) {
var key = keygen()
while (notesData.some((note) => note.key === key)) {
key = keygen()
}
var noteData = dummyNote({
key,
folder: jsonData.folders[Math.floor(Math.random() * jsonData.folders.length)].key
})
notesData.push(noteData)
}
notesData.forEach(function saveNoteCSON (note) {
CSON.writeFileSync(path.join(storagePath, 'notes', note.key + '.cson'), _.omit(note, ['key']))
})
return {
json: jsonData,
cache: cacheData,
notes: notesData
}
}
function dummyLegacyStorage (storagePath, override = {}) {
var jsonData = override.json != null
? override.json
: dummyBoostnoteJSONData({}, true)
var cacheData = override.cache != null
? override.cache
: {}
if (cacheData.key == null) cacheData.key = keygen()
if (cacheData.name == null) cacheData.name = faker.random.word()
if (cacheData.type == null) cacheData.type = 'FILESYSTEM'
cacheData.path = storagePath
sander.writeFileSync(path.join(storagePath, 'boostnote.json'), JSON.stringify(jsonData))
var notesData = []
for (var j = 0; j < jsonData.folders.length; j++) {
var folderNotes = []
var noteCount = Math.floor((Math.random() * 5)) + 1
for (var i = 0; i < noteCount; i++) {
var key = keygen(6)
while (folderNotes.some((note) => note.key === key)) {
key = keygen(6)
}
var noteData = dummyNote({
key,
folder: jsonData.folders[j].key
})
folderNotes.push(noteData)
}
notesData = notesData.concat(folderNotes)
CSON.writeFileSync(path.join(storagePath, jsonData.folders[j].key, 'data.json'), {notes: folderNotes.map((note) => _.omit(note, ['folder']))})
}
return {
json: jsonData,
cache: cacheData,
notes: notesData
}
}
module.exports = {
dummyFolder,
dummyBoostnoteJSONData,
dummyStorage,
dummyLegacyStorage
}

View File

@@ -24,7 +24,7 @@ var config = Object.assign({}, skeleton, {
publicPath: 'http://localhost:8080/assets/' publicPath: 'http://localhost:8080/assets/'
}, },
debug: true, debug: true,
devtool: 'eval-source-map' devtool: 'cheap-module-eval-source-map'
}) })
module.exports = config module.exports = config