diff --git a/browser/finder/NoteItem.js b/browser/finder/NoteItem.js index 9dd183ee..bc3ae253 100644 --- a/browser/finder/NoteItem.js +++ b/browser/finder/NoteItem.js @@ -34,7 +34,7 @@ class NoteItem extends React.Component { ? 'root--active' : 'root' } - key={note.uniqueKey} + key={note.storage + '-' + note.key} onClick={(e) => this.handleClick(e)} >
diff --git a/browser/finder/NoteList.js b/browser/finder/NoteList.js index 986efcf7..7087eb53 100644 --- a/browser/finder/NoteList.js +++ b/browser/finder/NoteList.js @@ -64,7 +64,7 @@ class NoteList extends React.Component { return ( { + data.storageMap.forEach((storage, index) => { storage.folders.forEach((folder) => { options.push({ storage: storage, diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 5c37074d..93b3a40a 100644 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -11,19 +11,17 @@ import ee from 'browser/main/lib/eventEmitter' const electron = require('electron') const { remote } = electron -const Menu = remote.Menu -const MenuItem = remote.MenuItem +const { Menu, MenuItem, dialog } = remote class MarkdownNoteDetail extends React.Component { constructor (props) { super(props) this.state = { + isMovingNote: false, note: Object.assign({ title: '', - content: '', - isMovingNote: false, - isDeleting: false + content: '' }, props.note) } this.dispatchTimer = null @@ -35,9 +33,9 @@ class MarkdownNoteDetail extends React.Component { componentWillReceiveProps (nextProps) { if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) { + if (this.saveQueue != null) this.saveNow() this.setState({ - note: Object.assign({}, nextProps.note), - isDeleting: false + note: Object.assign({}, nextProps.note) }, () => { this.refs.content.reload() this.refs.tags.reset() @@ -45,6 +43,10 @@ class MarkdownNoteDetail extends React.Component { } } + componentWillUnmount () { + if (this.saveQueue != null) this.saveNow() + } + findTitle (value) { let splitted = value.split('\n') let title = null @@ -91,17 +93,25 @@ class MarkdownNoteDetail extends React.Component { save () { clearTimeout(this.saveQueue) this.saveQueue = setTimeout(() => { - let { note, dispatch } = this.props - dispatch({ - type: 'UPDATE_NOTE', - note: this.state.note - }) - - dataApi - .updateNote(note.storage, note.folder, note.key, this.state.note) + this.saveNow() }, 1000) } + saveNow () { + let { note, dispatch } = this.props + clearTimeout(this.saveQueue) + this.saveQueue = null + + dataApi + .updateNote(note.storage, note.key, this.state.note) + .then((note) => { + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + }) + } + handleFolderChange (e) { let { note } = this.state let value = this.refs.folder.value @@ -110,7 +120,7 @@ class MarkdownNoteDetail extends React.Component { let newFolderKey = splitted.shift() dataApi - .moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey) + .moveNote(note.storage, note.key, newStorageKey, newFolderKey) .then((newNote) => { this.setState({ isMovingNote: true, @@ -119,13 +129,13 @@ class MarkdownNoteDetail extends React.Component { let { dispatch, location } = this.props dispatch({ type: 'MOVE_NOTE', - note: note, - newNote: newNote + originNote: note, + note: newNote }) hashHistory.replace({ pathname: location.pathname, query: { - key: newNote.uniqueKey + key: newNote.storage + '-' + newNote.key } }) this.setState({ @@ -175,34 +185,28 @@ class MarkdownNoteDetail extends React.Component { } handleDeleteMenuClick (e) { - this.setState({ - isDeleting: true - }, () => { - this.refs.deleteConfirmButton.focus() - }) - } - - handleDeleteConfirmButtonClick (e) { - let { note, dispatch } = this.props - dataApi - .removeNote(note.storage, note.folder, note.key) - .then(() => { - let dispatchHandler = () => { - dispatch({ - type: 'REMOVE_NOTE', - note: note - }) - } - ee.once('list:moved', dispatchHandler) - ee.emit('list:next') - ee.emit('list:focus') - }) - } - - handleDeleteCancelButtonClick (e) { - this.setState({ - isDeleting: false + let index = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'warning', + message: 'Delete a note', + detail: 'This work cannot be undone.', + buttons: ['Confirm', 'Cancel'] }) + if (index === 0) { + let { note, dispatch } = this.props + dataApi + .deleteNote(note.storage, note.key) + .then((data) => { + let dispatchHandler = () => { + dispatch({ + type: 'DELETE_NOTE', + storageKey: data.storageKey, + noteKey: data.noteKey + }) + } + ee.once('list:moved', dispatchHandler) + ee.emit('list:next') + }) + } } handleDeleteKeyDown (e) { @@ -210,7 +214,7 @@ class MarkdownNoteDetail extends React.Component { } render () { - let { storages, config } = this.props + let { data, config } = this.props let { note } = this.state return ( @@ -218,69 +222,50 @@ class MarkdownNoteDetail extends React.Component { style={this.props.style} styleName='root' > - {this.state.isDeleting - ?
-
this.handleDeleteKeyDown(e)} - > - - - Are you sure to delete this note? - - - -
-
- :
-
-
- this.handleFolderChange(e)} - /> -
-
- this.handleChange(e)} - /> -
-
-
- this.handleStarButtonClick(e)} - isActive={note.isStarred} +
+
+
+ this.handleFolderChange(e)} + /> +
+
+ this.handleChange(e)} /> - -
- } +
+ this.handleStarButtonClick(e)} + isActive={note.isStarred} + /> + + +
+
+
Object.assign({}, snippet)) - }), - isDeleting: false + }) } } @@ -48,6 +47,7 @@ class SnippetNoteDetail extends React.Component { componentWillReceiveProps (nextProps) { if (nextProps.note.key !== this.props.note.key) { + if (this.saveQueue != null) this.saveNow() let nextNote = Object.assign({ description: '' }, nextProps.note, { @@ -55,8 +55,7 @@ class SnippetNoteDetail extends React.Component { }) this.setState({ snippetIndex: 0, - note: nextNote, - isDeleting: false + note: nextNote }, () => { let { snippets } = this.state.note snippets.forEach((snippet, index) => { @@ -67,6 +66,10 @@ class SnippetNoteDetail extends React.Component { } } + componentWillUnmount () { + if (this.saveQueue != null) this.saveNow() + } + findTitle (value) { let splitted = value.split('\n') let title = null @@ -113,17 +116,25 @@ class SnippetNoteDetail extends React.Component { save () { clearTimeout(this.saveQueue) this.saveQueue = setTimeout(() => { - let { note, dispatch } = this.props - dispatch({ - type: 'UPDATE_NOTE', - note: this.state.note - }) - - dataApi - .updateNote(note.storage, note.folder, note.key, this.state.note) + this.saveNow() }, 1000) } + saveNow () { + let { note, dispatch } = this.props + clearTimeout(this.saveQueue) + this.saveQueue = null + + dataApi + .updateNote(note.storage, note.key, this.state.note) + .then((note) => { + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + }) + } + handleFolderChange (e) { let { note } = this.state let value = this.refs.folder.value @@ -132,7 +143,7 @@ class SnippetNoteDetail extends React.Component { let newFolderKey = splitted.shift() dataApi - .moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey) + .moveNote(note.storage, note.key, newStorageKey, newFolderKey) .then((newNote) => { this.setState({ isMovingNote: true, @@ -141,13 +152,13 @@ class SnippetNoteDetail extends React.Component { let { dispatch, location } = this.props dispatch({ type: 'MOVE_NOTE', - note: note, - newNote: newNote + originNote: note, + note: newNote }) hashHistory.replace({ pathname: location.pathname, query: { - key: newNote.uniqueKey + key: newNote.storage + '-' + newNote.key } }) this.setState({ @@ -198,33 +209,28 @@ class SnippetNoteDetail extends React.Component { } handleDeleteMenuClick (e) { - this.setState({ - isDeleting: true - }, () => { - this.refs.deleteConfirmButton.focus() - }) - } - - handleDeleteConfirmButtonClick (e) { - let { note, dispatch } = this.props - dataApi - .removeNote(note.storage, note.folder, note.key) - .then(() => { - let dispatchHandler = () => { - dispatch({ - type: 'REMOVE_NOTE', - note: note - }) - } - ee.once('list:moved', dispatchHandler) - ee.emit('list:next') - }) - } - - handleDeleteCancelButtonClick (e) { - this.setState({ - isDeleting: false + let index = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'warning', + message: 'Delete a note', + detail: 'This work cannot be undone.', + buttons: ['Confirm', 'Cancel'] }) + if (index === 0) { + let { note, dispatch } = this.props + dataApi + .deleteNote(note.storage, note.key) + .then((data) => { + let dispatchHandler = () => { + dispatch({ + type: 'DELETE_NOTE', + storageKey: data.storageKey, + noteKey: data.noteKey + }) + } + ee.once('list:moved', dispatchHandler) + ee.emit('list:next') + }) + } } handleTabPlusButtonClick (e) { @@ -321,7 +327,7 @@ class SnippetNoteDetail extends React.Component { } render () { - let { storages, config } = this.props + let { data, config } = this.props let { note } = this.state let editorFontSize = parseInt(config.editor.fontSize, 10) @@ -410,68 +416,49 @@ class SnippetNoteDetail extends React.Component { style={this.props.style} styleName='root' > - {this.state.isDeleting - ?
-
this.handleDeleteKeyDown(e)} - > - - Are you sure to delete this note? - - - -
-
- :
-
-
- this.handleFolderChange(e)} - /> -
-
- this.handleChange(e)} - /> -
-
-
- this.handleStarButtonClick(e)} - isActive={note.isStarred} +
+
+
+ this.handleFolderChange(e)} + /> +
+
+ this.handleChange(e)} /> - -
- } +
+ this.handleStarButtonClick(e)} + isActive={note.isStarred} + /> + + +
+
diff --git a/browser/main/Detail/SnippetNoteDetail.styl b/browser/main/Detail/SnippetNoteDetail.styl index 5a875180..cd1351db 100644 --- a/browser/main/Detail/SnippetNoteDetail.styl +++ b/browser/main/Detail/SnippetNoteDetail.styl @@ -12,34 +12,6 @@ $info-height = 75px border-bottom $ui-border 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 float left padding 0 5px diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js index d2c96726..89d1dd56 100644 --- a/browser/main/Detail/index.js +++ b/browser/main/Detail/index.js @@ -31,19 +31,14 @@ class Detail extends React.Component { } render () { - let { location, notes, config } = this.props + let { location, data, config } = this.props let note = null if (location.query.key != null) { let splitted = location.query.key.split('-') let storageKey = splitted.shift() - let folderKey = splitted.shift() let noteKey = splitted.shift() - note = _.find(notes, { - storage: storageKey, - folder: folderKey, - key: noteKey - }) + note = data.noteMap.get(storageKey + '-' + noteKey) } if (note == null) { @@ -67,7 +62,7 @@ class Detail extends React.Component { ref='root' {..._.pick(this.props, [ 'dispatch', - 'storages', + 'data', 'style', 'ignorePreviewPointerEvents', 'location' @@ -83,7 +78,7 @@ class Detail extends React.Component { ref='root' {..._.pick(this.props, [ 'dispatch', - 'storages', + 'data', 'style', 'ignorePreviewPointerEvents', 'location' @@ -95,7 +90,6 @@ class Detail extends React.Component { Detail.propTypes = { dispatch: PropTypes.func, - storages: PropTypes.array, style: PropTypes.shape({ left: PropTypes.number }), diff --git a/browser/main/Main.js b/browser/main/Main.js index 45295a4d..9c3138e1 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -101,7 +101,7 @@ class Main extends React.Component { x)(CSSModules(Main, styles)) diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index c44a5bdd..df33ed98 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -43,7 +43,7 @@ class NoteList extends React.Component { router.replace({ pathname: location.pathname, query: { - key: this.notes[0].uniqueKey + key: this.notes[0].storage + '-' + this.notes[0].key } }) return @@ -52,7 +52,7 @@ class NoteList extends React.Component { // Auto scroll if (_.isString(location.query.key)) { 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) { let list = this.refs.root @@ -78,7 +78,7 @@ class NoteList extends React.Component { let { location } = this.props let targetIndex = _.findIndex(this.notes, (note) => { - return note.uniqueKey === location.query.key + return note.storage + '-' + note.key === location.query.key }) if (targetIndex === 0) { @@ -90,7 +90,7 @@ class NoteList extends React.Component { router.push({ pathname: location.pathname, 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 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) { @@ -117,7 +117,7 @@ class NoteList extends React.Component { router.push({ pathname: location.pathname, query: { - key: this.notes[targetIndex].uniqueKey + key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key } }) ee.emit('list:moved') @@ -153,30 +153,36 @@ class NoteList extends React.Component { } getNotes () { - let { storages, notes, params, location } = this.props + let { data, params, location } = this.props if (location.pathname.match(/\/home/)) { - return notes + return data.noteMap.map((note) => note) } if (location.pathname.match(/\/starred/)) { - return notes - .filter((note) => note.isStarred) + return data.starredSet.toJS() + .map((uniqueKey) => data.noteMap.get(uniqueKey)) } let storageKey = params.storageKey let folderKey = params.folderKey - let storage = _.find(storages, {key: storageKey}) + let storage = data.storageMap.get(storageKey) if (storage == null) return [] let folder = _.find(storage.folders, {key: folderKey}) if (folder == null) { - return notes - .filter((note) => note.storage === storageKey) + return data.storageNoteMap + .get(storage.key) + .map((uniqueKey) => data.noteMap.get(uniqueKey)) } - return notes - .filter((note) => note.folder === folderKey) + let folderNoteKeyList = data.folderNoteMap + .get(storage.key + '-' + folder.key) + + return folderNoteKeyList != null + ? folderNoteKeyList + .map((uniqueKey) => data.noteMap.get(uniqueKey)) + : [] } handleNoteClick (uniqueKey) { @@ -194,13 +200,14 @@ class NoteList extends React.Component { } render () { - let { location, storages, notes } = this.props + let { location, data, notes } = this.props this.notes = notes = this.getNotes() .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)) let noteList = notes .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 tagElements = _.isArray(note.tags) ? 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 (
this.handleNoteClick(note.uniqueKey)(e)} + key={note.storage + '-' + note.key} + onClick={(e) => this.handleNoteClick(note.storage + '-' + note.key)(e)} >
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 37bf994a..3e541413 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -36,12 +36,13 @@ class SideNav extends React.Component { } render () { - let { storages, location, config } = this.props + let { data, location, config } = this.props let isFolded = config.isSideNavFolded let isHomeActive = location.pathname.match(/^\/home$/) let isStarredActive = location.pathname.match(/^\/starred$/) - let storageList = storages.map((storage) => { + + let storageList = data.storageMap.map((storage, key) => { return 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 diff --git a/browser/main/lib/dataApi/createNote.js b/browser/main/lib/dataApi/createNote.js new file mode 100644 index 00000000..b98f6832 --- /dev/null +++ b/browser/main/lib/dataApi/createNote.js @@ -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 diff --git a/browser/main/lib/dataApi/deleteFolder.js b/browser/main/lib/dataApi/deleteFolder.js new file mode 100644 index 00000000..f001ab59 --- /dev/null +++ b/browser/main/lib/dataApi/deleteFolder.js @@ -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 diff --git a/browser/main/lib/dataApi/deleteNote.js b/browser/main/lib/dataApi/deleteNote.js new file mode 100644 index 00000000..119e1952 --- /dev/null +++ b/browser/main/lib/dataApi/deleteNote.js @@ -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 diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 464d05d2..bad6f527 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -1,566 +1,20 @@ -const keygen = require('browser/lib/keygen') -const CSON = require('season') -const path = require('path') -const _ = require('lodash') -const sander = require('sander') -const consts = require('browser/lib/consts') +const dataApi = { + init: require('./init'), + addStorage: require('./addStorage'), + renameStorage: require('./renameStorage'), + removeStorage: require('./removeStorage'), + 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 = [] -let notes = [] - -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 - }) + _migrateFromV6Storage: require('./migrateFromV6Storage'), + _resolveStorageData: require('./resolveStorageData'), + _resolveStorageNotes: require('./resolveStorageNotes') } -class Storage { - 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 -} +module.exports = dataApi diff --git a/browser/main/lib/dataApi/init.js b/browser/main/lib/dataApi/init.js index 316ea56d..7488dce0 100644 --- a/browser/main/lib/dataApi/init.js +++ b/browser/main/lib/dataApi/init.js @@ -1,8 +1,21 @@ 'use strict' const _ = require('lodash') -const sander = require('sander') -const path = require('path') - +const resolveStorageData = require('./resolveStorageData') +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 () { let fetchStorages = function () { let rawStorages @@ -10,67 +23,38 @@ function init () { rawStorages = JSON.parse(window.localStorage.getItem('storages')) if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.') } catch (e) { - console.error(e) + console.warn('Failed to parse cached data from localStorage', e) rawStorages = [] window.localStorage.setItem('storages', JSON.stringify(rawStorages)) } return Promise.all(rawStorages - .map(function assignFoldersToStorage (rawStorage) { - 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) - })) + .map(resolveStorageData)) } let fetchNotes = function (storages) { - let notes = [] - - storages - .forEach((storage) => { - storage.folders.forEach((folder) => { - let dataPath = path.join(storage.path, folder.key, 'data.json') - 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) - }) - }) + let findNotesFromEachStorage = storages + .map(resolveStorageNotes) + return Promise.all(findNotesFromEachStorage) + .then(function concatNoteGroup (noteGroups) { + return noteGroups.reduce(function (sum, group) { + return sum.concat(group) + }, []) + }) + .then(function returnData (notes) { + return { + storages, + notes + } }) - return Promise.resolve({ - storages, - notes - }) } return Promise.resolve(fetchStorages()) .then((storages) => { - storages = storages.filter((storage) => { - if (!_.isObject(storage)) return false - return true - }) return storages + .filter((storage) => { + if (!_.isObject(storage)) return false + return true + }) }) .then(fetchNotes) } diff --git a/browser/main/lib/dataApi/migrateFromV5Storage.js b/browser/main/lib/dataApi/migrateFromV5Storage.js new file mode 100644 index 00000000..d13a5e28 --- /dev/null +++ b/browser/main/lib/dataApi/migrateFromV5Storage.js @@ -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 diff --git a/browser/main/lib/dataApi/migrateFromV6Storage.js b/browser/main/lib/dataApi/migrateFromV6Storage.js new file mode 100644 index 00000000..1af49cf5 --- /dev/null +++ b/browser/main/lib/dataApi/migrateFromV6Storage.js @@ -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 + diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js new file mode 100644 index 00000000..1564d80c --- /dev/null +++ b/browser/main/lib/dataApi/moveNote.js @@ -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 diff --git a/browser/main/lib/dataApi/removeStorage.js b/browser/main/lib/dataApi/removeStorage.js new file mode 100644 index 00000000..c50bbd12 --- /dev/null +++ b/browser/main/lib/dataApi/removeStorage.js @@ -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 diff --git a/browser/main/lib/dataApi/renameStorage.js b/browser/main/lib/dataApi/renameStorage.js new file mode 100644 index 00000000..5ab86bcb --- /dev/null +++ b/browser/main/lib/dataApi/renameStorage.js @@ -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 diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js new file mode 100644 index 00000000..1cf409a1 --- /dev/null +++ b/browser/main/lib/dataApi/resolveStorageData.js @@ -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 diff --git a/browser/main/lib/dataApi/resolveStorageNotes.js b/browser/main/lib/dataApi/resolveStorageNotes.js new file mode 100644 index 00000000..68885fce --- /dev/null +++ b/browser/main/lib/dataApi/resolveStorageNotes.js @@ -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 diff --git a/browser/main/lib/dataApi/updateFolder.js b/browser/main/lib/dataApi/updateFolder.js new file mode 100644 index 00000000..44250b2e --- /dev/null +++ b/browser/main/lib/dataApi/updateFolder.js @@ -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 diff --git a/browser/main/lib/dataApi/updateNote.js b/browser/main/lib/dataApi/updateNote.js new file mode 100644 index 00000000..6d132e99 --- /dev/null +++ b/browser/main/lib/dataApi/updateNote.js @@ -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 diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index f8793fdc..a4562eaf 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -24,23 +24,26 @@ class NewNoteModal extends React.Component { handleMarkdownNoteButtonClick (e) { let { storage, folder, dispatch, location } = this.props dataApi - .createMarkdownNote(storage, folder, { + .createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, title: '', content: '' }) .then((note) => { dispatch({ - type: 'CREATE_NOTE', + type: 'UPDATE_NOTE', note: note }) hashHistory.push({ pathname: location.pathname, - query: {key: note.uniqueKey} + query: {key: note.storage + '-' + note.key} }) ee.emit('detail:focus') this.props.close() }) } + handleMarkdownNoteButtonKeyDown (e) { if (e.keyCode === 9) { e.preventDefault() @@ -50,8 +53,11 @@ class NewNoteModal extends React.Component { handleSnippetNoteButtonClick (e) { let { storage, folder, dispatch, location } = this.props + dataApi - .createSnippetNote(storage, folder, { + .createNote(storage, { + type: 'SNIPPET_NOTE', + folder: folder, title: '', description: '', snippets: [{ @@ -62,12 +68,12 @@ class NewNoteModal extends React.Component { }) .then((note) => { dispatch({ - type: 'CREATE_NOTE', + type: 'UPDATE_NOTE', note: note }) hashHistory.push({ pathname: location.pathname, - query: {key: note.uniqueKey} + query: {key: note.storage + '-' + note.key} }) ee.emit('detail:focus') this.props.close() diff --git a/browser/main/modals/PreferencesModal/StorageItem.js b/browser/main/modals/PreferencesModal/StorageItem.js index 68cfb1fe..314e5cba 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.js +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -7,8 +7,7 @@ import dataApi from 'browser/main/lib/dataApi' import store from 'browser/main/store' const electron = require('electron') -const { shell, remote } = electron -const { Menu, MenuItem } = remote +const { shell } = electron import { SketchPicker } from 'react-color' class UnstyledFolderItem extends React.Component { @@ -42,10 +41,10 @@ class UnstyledFolderItem extends React.Component { color: this.state.folder.color, name: this.state.folder.name }) - .then((storage) => { + .then((data) => { store.dispatch({ - type: 'UPDATE_STORAGE', - storage: storage + type: 'UPDATE_FOLDER', + storage: data.storage }) this.setState({ status: 'IDLE' @@ -55,10 +54,10 @@ class UnstyledFolderItem extends React.Component { handleColorButtonClick (e) { 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 // by comparing its dimensions to the host dimensions. - const { hostBoundingBox } = this.props; + const { hostBoundingBox } = this.props const colorPickerNode = ReactDOM.findDOMNode(this.refs.colorPicker) const colorPickerBox = colorPickerNode.getBoundingClientRect() const offsetTop = hostBoundingBox.bottom - colorPickerBox.bottom @@ -103,19 +102,22 @@ class UnstyledFolderItem extends React.Component { { + .deleteFolder(storage.key, folder.key) + .then((data) => { store.dispatch({ - type: 'REMOVE_FOLDER', - key: folder.key, - storage: storage + type: 'DELETE_FOLDER', + storage: data.storage, + folderKey: data.folderKey }) }) } @@ -254,10 +256,10 @@ class StorageItem extends React.Component { } dataApi.createFolder(storage.key, input) - .then((storage) => { + .then((data) => { store.dispatch({ - type: 'ADD_FOLDER', - storage: storage + type: 'UPDATE_FOLDER', + storage: data.storage }) }) .catch((err) => { @@ -276,11 +278,11 @@ class StorageItem extends React.Component { .then(() => { store.dispatch({ type: 'REMOVE_STORAGE', - key: storage.key + storageKey: storage.key }) }) .catch((err) => { - console.error(err) + throw err }) } diff --git a/browser/main/modals/PreferencesModal/StoragesTab.js b/browser/main/modals/PreferencesModal/StoragesTab.js index ead759ba..f0609e6e 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.js +++ b/browser/main/modals/PreferencesModal/StoragesTab.js @@ -51,14 +51,13 @@ class StoragesTab extends React.Component { } renderList () { - let { storages, boundingBox } = this.props + let { data, boundingBox } = this.props if (!boundingBox) { return null } - let storageList = storages.map((storage) => { + let storageList = data.storageMap.map((storage) => { return }) diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js index fead28f4..0f8382e1 100644 --- a/browser/main/modals/PreferencesModal/index.js +++ b/browser/main/modals/PreferencesModal/index.js @@ -33,8 +33,8 @@ class Preferences extends React.Component { } renderContent () { - const { boundingBox } = this.state; - let { dispatch, config, storages } = this.props + const { boundingBox } = this.state + let { dispatch, config, data } = this.props switch (this.state.currentTab) { case 'INFO': @@ -51,7 +51,7 @@ class Preferences extends React.Component { return ( ) @@ -66,7 +66,7 @@ class Preferences extends React.Component { getContentBoundingBox () { const node = ReactDOM.findDOMNode(this.refs.content) - return node.getBoundingClientRect(); + return node.getBoundingClientRect() } render () { diff --git a/browser/main/store.js b/browser/main/store.js index b1c2e68b..8b778939 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -1,95 +1,449 @@ import { combineReducers, createStore } from 'redux' import { routerReducer } from 'react-router-redux' import ConfigManager from 'browser/main/lib/ConfigManager' +import { Map, Set } from 'browser/lib/Mutable' +import _ from 'lodash' -function storages (state = [], action) { - console.info('REDUX >> ', action) - switch (action.type) { - case 'INIT_ALL': - return action.storages - case 'ADD_STORAGE': - { - let storages = state.slice() - - 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 - } +function defaultDataMap () { + return { + storageMap: new Map(), + noteMap: new Map(), + starredSet: new Set(), + storageNoteMap: new Map(), + folderNoteMap: new Map(), + tagNoteMap: new Map() } - return state } -function notes (state = [], action) { +function data (state = defaultDataMap(), action) { switch (action.type) { case 'INIT_ALL': - return action.notes - 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) + state = defaultDataMap() - return notes - } - case 'REMOVE_FOLDER': - { - let notes = state.slice() - notes = notes - .filter((note) => note.storage !== action.storage.key || note.folder !== action.key) + action.storages.forEach((storage) => { + state.storageMap.set(storage.key, storage) + }) - return notes - } - case 'CREATE_NOTE': - { - let notes = state.slice() - notes.push(action.note) - 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) + } + + 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 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': { - let notes = state.slice() - notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage) - notes.push(action.note) - return notes + let note = action.note + 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.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': { - let notes = state.slice() - notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage) - notes.push(action.newNote) - return notes + let originNote = action.originNote + let originKey = originNote.storage + '-' + originNote.key + let note = action.note + 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) + } + + // 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 'REMOVE_NOTE': + case 'DELETE_NOTE': { - let notes = state.slice() - notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage) - return notes + let uniqueKey = action.storageKey + '-' + action.noteKey + let targetNote = state.noteMap.get(uniqueKey) + + 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 } @@ -116,8 +470,7 @@ function config (state = defaultConfig, action) { } let reducer = combineReducers({ - storages, - notes, + data, config, routing: routerReducer }) diff --git a/package.json b/package.json index 29f4d696..039d179e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "electron-gh-releases": "^2.0.2", "font-awesome": "^4.3.0", "highlight.js": "^9.3.0", + "immutable": "^3.8.1", "lodash": "^4.11.1", "markdown-it": "^6.0.1", "markdown-it-checkbox": "^1.1.0", @@ -76,6 +77,7 @@ "dom-storage": "^2.0.2", "electron-packager": "^6.0.0", "electron-prebuilt": "^1.2.8", + "faker": "^3.1.0", "grunt": "^0.4.5", "grunt-electron-installer": "^1.2.0", "history": "^1.17.0", diff --git a/tests/dataApi/addStorage.js b/tests/dataApi/addStorage.js index 26d24fe9..1ca10c54 100644 --- a/tests/dataApi/addStorage.js +++ b/tests/dataApi/addStorage.js @@ -8,86 +8,65 @@ 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 _ = require('lodash') +const os = require('os') +const CSON = require('season') -function copyFile (filePath, targetPath) { - return sander.readFile(filePath) - .then(function writeFile (data) { - return sander.writeFile(targetPath, data.toString()) - }) -} +const v1StoragePath = path.join(os.tmpdir(), 'test/addStorage-v1-storage') +// const legacyStoragePath = path.join(os.tmpdir(), 'test/addStorage-legacy-storage') +// const emptyDirPath = path.join(os.tmpdir(), 'test/addStorage-empty-storage') -test('add a initialized storage', (t) => { - const dummyStoragePath = path.join(__dirname, '../dummy/dummyStorage') - const targetPath = path.join(__dirname, '../sandbox/test-add-storage1') +test.beforeEach((t) => { + t.context.v1StorageData = TestDummy.dummyStorage(v1StoragePath) + // t.context.legacyStorageData = TestDummy.dummyLegacyStorage(legacyStoragePath) + + localStorage.setItem('storages', JSON.stringify([])) +}) + +test.serial('Add Storage', (t) => { const input = { type: 'FILESYSTEM', - name: 'test-add-storage1', - path: targetPath + name: 'add-storage1', + path: v1StoragePath } return Promise.resolve() - .then(function before () { - 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) { + .then(function doTest () { return addStorage(input) }) .then(function validateResult (data) { - const { storage, notes } = data + let { storage, notes } = data + // Check data.storage t.true(_.isString(storage.key)) - t.is(storage.name, 'test-add-storage1') - t.true(_.isArray(storage.folders)) - t.is(storage.folders.length, 1) - t.true(_.isArray(notes)) - t.is(notes.length, 2) - t.is(notes[0].folder, 'fc6ba88e8ecf') - t.is(notes[0].storage, storage.key) - }) - .then(function after () { - localStorage.clear() - sander.rimrafSync(targetPath) + t.is(storage.name, input.name) + t.is(storage.type, input.type) + t.is(storage.path, input.path) + t.is(storage.version, '1.0') + t.is(storage.folders.length, t.context.v1StorageData.json.folders.length) + + // Check data.notes + t.is(notes.length, t.context.v1StorageData.notes.length) + notes.forEach(function validateNote (note) { + t.is(note.storage, storage.key) + }) + + // Check localStorage + 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) => { - 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() - sander.rimrafSync(targetPath) - }) +test.after.always(() => { + localStorage.clear() + sander.rimrafSync(v1StoragePath) }) diff --git a/tests/dataApi/createFolder.js b/tests/dataApi/createFolder.js new file mode 100644 index 00000000..986123b0 --- /dev/null +++ b/tests/dataApi/createFolder.js @@ -0,0 +1,45 @@ +const test = require('ava') +const createFolder = require('browser/main/lib/dataApi/createFolder') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/createNote.js b/tests/dataApi/createNote.js new file mode 100644 index 00000000..7a983fef --- /dev/null +++ b/tests/dataApi/createNote.js @@ -0,0 +1,89 @@ +const test = require('ava') +const createNote = require('browser/main/lib/dataApi/createNote') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/deleteFolder.js b/tests/dataApi/deleteFolder.js new file mode 100644 index 00000000..f760c819 --- /dev/null +++ b/tests/dataApi/deleteFolder.js @@ -0,0 +1,45 @@ +const test = require('ava') +const deleteFolder = require('browser/main/lib/dataApi/deleteFolder') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/deleteNote.js b/tests/dataApi/deleteNote.js new file mode 100644 index 00000000..b22e5de2 --- /dev/null +++ b/tests/dataApi/deleteNote.js @@ -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('') +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) +}) diff --git a/tests/dataApi/init.js b/tests/dataApi/init.js index 2d47a904..81ef38a4 100644 --- a/tests/dataApi/init.js +++ b/tests/dataApi/init.js @@ -8,68 +8,58 @@ 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 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 dummyStoragePath = path.join(__dirname, '..', 'dummy/dummyStorage') - const dummyRawStorage = { - name: 'test1', - key: crypto.randomBytes(6).toString('hex'), - path: dummyStoragePath +const v1StoragePath = path.join(os.tmpdir(), 'test/init-v1-storage') +const legacyStoragePath = path.join(os.tmpdir(), 'test/init-legacy-storage') +const emptyDirPath = path.join(os.tmpdir(), 'test/init-empty-storage') + +test.beforeEach((t) => { + 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() - .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.is(data.notes.length, 2) - data.notes.forEach((note) => { - t.is(note.folder, dummyFolderKey) + t.is(data.notes.length, v1StorageData.notes.length + legacyStorageData.notes.length) + t.is(data.storages.length, 3) + data.storages.forEach(function assertStorage (storage) { + t.true(_.isString(storage.key)) + t.true(_.isString(storage.name)) + t.true(storage.type === 'FILESYSTEM') + t.true(_.isString(storage.path)) }) - - t.true(Array.isArray(data.notes)) }) .then(function after () { localStorage.clear() }) }) -test.serial('If storage path is a empty folder, return metadata with empty folder array and empty note array.', (t) => { - 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() - }) +test.after.always(() => { + localStorage.clear() + sander.rimrafSync(v1StoragePath) + sander.rimrafSync(legacyStoragePath) + sander.rimrafSync(emptyDirPath) }) diff --git a/tests/dataApi/migrateFromV6Storage.js b/tests/dataApi/migrateFromV6Storage.js new file mode 100644 index 00000000..f10e3ff6 --- /dev/null +++ b/tests/dataApi/migrateFromV6Storage.js @@ -0,0 +1,64 @@ +const test = require('ava') +const migrateFromV6Storage = require('browser/main/lib/dataApi/migrateFromV6Storage') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/moveNote.js b/tests/dataApi/moveNote.js new file mode 100644 index 00000000..595b46ed --- /dev/null +++ b/tests/dataApi/moveNote.js @@ -0,0 +1,66 @@ +const test = require('ava') +const moveNote = require('browser/main/lib/dataApi/moveNote') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/removeStorage.js b/tests/dataApi/removeStorage.js new file mode 100644 index 00000000..33541df1 --- /dev/null +++ b/tests/dataApi/removeStorage.js @@ -0,0 +1,36 @@ +const test = require('ava') +const removeStorage = require('browser/main/lib/dataApi/removeStorage') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/renameStorage.js b/tests/dataApi/renameStorage.js new file mode 100644 index 00000000..b898d6f1 --- /dev/null +++ b/tests/dataApi/renameStorage.js @@ -0,0 +1,38 @@ +const test = require('ava') +const renameStorage = require('browser/main/lib/dataApi/renameStorage') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/updateFolder.js b/tests/dataApi/updateFolder.js new file mode 100644 index 00000000..9ebbdc9f --- /dev/null +++ b/tests/dataApi/updateFolder.js @@ -0,0 +1,46 @@ +const test = require('ava') +const updateFolder = require('browser/main/lib/dataApi/updateFolder') + +global.document = require('jsdom').jsdom('') +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) +}) diff --git a/tests/dataApi/updateNote.js b/tests/dataApi/updateNote.js new file mode 100644 index 00000000..63b74dbb --- /dev/null +++ b/tests/dataApi/updateNote.js @@ -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('') +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) +}) diff --git a/tests/dummy/dummyStorage/boostnote.json b/tests/dummy/dummyStorage/boostnote.json deleted file mode 100644 index ae4e4df4..00000000 --- a/tests/dummy/dummyStorage/boostnote.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "folders": [ - { - "key": "fc6ba88e8ecf", - "name": "test", - "color": "#FF5555" - } - ] -} diff --git a/tests/dummy/dummyStorage/fc6ba88e8ecf/data.json b/tests/dummy/dummyStorage/fc6ba88e8ecf/data.json deleted file mode 100644 index 08e002c1..00000000 --- a/tests/dummy/dummyStorage/fc6ba88e8ecf/data.json +++ /dev/null @@ -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" - } - ] -} diff --git a/tests/fixtures/TestDummy.js b/tests/fixtures/TestDummy.js new file mode 100644 index 00000000..19cfa9c5 --- /dev/null +++ b/tests/fixtures/TestDummy.js @@ -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 +} diff --git a/webpack.config.js b/webpack.config.js index 7a1768f1..dfde7e85 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,7 +24,7 @@ var config = Object.assign({}, skeleton, { publicPath: 'http://localhost:8080/assets/' }, debug: true, - devtool: 'eval-source-map' + devtool: 'cheap-module-eval-source-map' }) module.exports = config