diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index ad7eb5909..1556a56b3 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -70,7 +70,8 @@ class MarkdownNoteDetail extends React.Component { componentWillReceiveProps (nextProps) { const isNewNote = nextProps.note.key !== this.props.note.key const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length - if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) { + const updateContent = nextProps.note.content !== this.props.note.content + if (!this.state.isMovingNote && (isNewNote || hasDeletedTags || updateContent)) { if (this.saveQueue != null) this.saveNow() this.setState({ note: Object.assign({linesHighlighted: []}, nextProps.note) diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index ca513c044..37fb77a69 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -25,6 +25,7 @@ import context from 'browser/lib/context' const { remote } = require('electron') const { dialog } = remote const WP_POST_PATH = '/wp/v2/posts' +const CSON = require('@rokt33r/season') function sortByCreatedAt (a, b) { return new Date(b.createdAt) - new Date(a.createdAt) @@ -66,6 +67,7 @@ class NoteList extends React.Component { this.alertIfSnippetHandler = (event, msg) => { this.alertIfSnippet(msg) } + this.openFileHandler = this.openFile.bind(this) this.importFromFileHandler = this.importFromFile.bind(this) this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this) this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this) @@ -90,6 +92,8 @@ class NoteList extends React.Component { } this.contextNotes = [] + + this.fsWatcher = null } componentDidMount () { @@ -100,6 +104,7 @@ class NoteList extends React.Component { ee.on('list:focus', this.focusHandler) ee.on('list:isMarkdownNote', this.alertIfSnippetHandler) ee.on('import:file', this.importFromFileHandler) + ee.on('open:file', this.openFileHandler) ee.on('list:jump', this.jumpNoteByHash) ee.on('list:navigate', this.navigate) } @@ -127,13 +132,66 @@ class NoteList extends React.Component { } componentDidUpdate (prevProps) { - const { location } = this.props + const { location, dispatch } = this.props + const { storage } = this.resolveTargetFolder() const { selectedNoteKeys } = this.state const visibleNoteKeys = this.notes.map(note => note.key) const note = this.notes[0] const prevKey = prevProps.location.query.key const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key + if (location.query.key !== prevKey) { + let focusNote = findNoteByKey(this.notes, location.query.key) + + if (focusNote.filepath !== undefined) { + try { + if (this.fsWatcher != null) { + this.fsWatcher.close() + this.fsWatcher = null + } + } catch (err) { + console.error('File watcher does not exist: ' + err) + } + + if (fs.existsSync(focusNote.filepath) && fs.lstatSync(focusNote.filepath).isFile()) { + this.fsWatcher = fs.watch(focusNote.filepath) + + this.fsWatcher.on('change', (event, filename) => { + let updatedNote = Object.assign({}, focusNote) + + switch (event) { + case 'change' : + updatedNote.content = fs.readFileSync(updatedNote.filepath, 'utf8') + updatedNote.contentSynced = true + break + + case 'rename': + updatedNote.filepath = undefined + try { + this.fsWatcher.close() + } catch (err) { + console.error('File watcher does not exist: ' + err) + } + const notePath = path.join(storage.path, 'notes', noteKey + '.cson') + CSON.writeFileSync(notePath, _.omit(updatedNote, ['key', 'storage'])) + break + + default: break + } + + dataApi + .updateNote(storage.key, focusNote.key, updatedNote) + .then((note) => { + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + }) + }) + } + } + } + if (note && location.query.key == null) { const { router } = this.context if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes() @@ -856,6 +914,19 @@ class NoteList extends React.Component { shell.openExternal(note.blog.blogLink) } + openFile () { + const options = { + filters: [ + { name: 'Documents', extensions: ['md', 'txt'] } + ], + properties: ['openFile'] + } + + dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => { + this.addNotesFromFiles(filepaths, 'LINK') + }) + } + importFromFile () { const options = { filters: [ @@ -865,7 +936,7 @@ class NoteList extends React.Component { } dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => { - this.addNotesFromFiles(filepaths) + this.addNotesFromFiles(filepaths, 'COPY') }) } @@ -873,11 +944,11 @@ class NoteList extends React.Component { e.preventDefault() const { location } = this.props const filepaths = Array.from(e.dataTransfer.files).map(file => { return file.path }) - if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths) + if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths, 'COPY') } // Add notes to the current folder - addNotesFromFiles (filepaths) { + addNotesFromFiles (filepaths, linktype) { const { dispatch, location } = this.props const { storage, folder } = this.resolveTargetFolder() @@ -896,7 +967,8 @@ class NoteList extends React.Component { title: path.basename(filepath, path.extname(filepath)), type: 'MARKDOWN_NOTE', createdAt: birthtime, - updatedAt: mtime + updatedAt: mtime, + filepath: (linktype === 'LINK' ? filepath : undefined) } dataApi.createNote(storage.key, newNote) .then((note) => { @@ -908,6 +980,8 @@ class NoteList extends React.Component { pathname: location.pathname, query: {key: getNoteKey(note)} }) + ee.emit('list:jump', getNoteKey(note)) + ee.emit('detail:focus') }) }) }) diff --git a/browser/main/lib/dataApi/updateNote.js b/browser/main/lib/dataApi/updateNote.js index ce9fabcff..c62531445 100644 --- a/browser/main/lib/dataApi/updateNote.js +++ b/browser/main/lib/dataApi/updateNote.js @@ -2,6 +2,7 @@ const resolveStorageData = require('./resolveStorageData') const _ = require('lodash') const path = require('path') const CSON = require('@rokt33r/season') +const fs = require('fs-plus') const { findStorage } = require('browser/lib/findStorage') function validateInput (input) { @@ -78,6 +79,7 @@ function updateNote (storageKey, noteKey, input) { let targetStorage try { if (input == null) throw new Error('No input found.') + var contentSynced = input.contentSynced input = validateInput(input) targetStorage = findStorage(storageKey) @@ -132,6 +134,11 @@ function updateNote (storageKey, noteKey, input) { CSON.writeFileSync(path.join(storage.path, 'notes', noteKey + '.cson'), _.omit(noteData, ['key', 'storage'])) + if (noteData.filepath !== undefined) { + if (!contentSynced && !noteData.isTrashed) { + fs.writeFileSync(noteData.filepath, noteData.content) + } + } return noteData }) } diff --git a/lib/main-menu.js b/lib/main-menu.js index dcd85217c..6a50141dd 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -76,6 +76,12 @@ const boost = macOS const file = { label: 'File', submenu: [ + { + label: 'Open File...', + click () { + mainWindow.webContents.send('open:file') + } + }, { label: 'New Note', accelerator: 'CommandOrControl+N', diff --git a/tests/dataApi/updateNote-test.js b/tests/dataApi/updateNote-test.js index da47c30c2..1c36c24af 100644 --- a/tests/dataApi/updateNote-test.js +++ b/tests/dataApi/updateNote-test.js @@ -13,6 +13,7 @@ const TestDummy = require('../fixtures/TestDummy') const sander = require('sander') const os = require('os') const CSON = require('@rokt33r/season') +const fs = require('fs') const faker = require('faker') const storagePath = path.join(os.tmpdir(), 'test/update-note') @@ -73,25 +74,45 @@ test.serial('Update a note', (t) => { } input4.title = input4.content.split('\n').shift() + const input5 = { + type: 'MARKDOWN_NOTE', + content: faker.lorem.lines(), + tags: faker.lorem.words().split(' '), + folder: folderKey, + filepath: path.join(storagePath, 'test', 'test' + '.txt') + } + input5.title = input5.content.split('\n').shift() + + const input6 = { + type: 'MARKDOWN_NOTE', + content: faker.lorem.lines(), + tags: faker.lorem.words().split(' ') + } + input6.title = input6.content.split('\n').shift() + return Promise.resolve() .then(function doTest () { return Promise .all([ createNote(storageKey, input1), - createNote(storageKey, input2) + createNote(storageKey, input2), + createNote(storageKey, input5) ]) .then(function updateNotes (data) { const data1 = data[0] const data2 = data[1] + const data3 = data[2] return Promise.all([ updateNote(data1.storage, data1.key, input3), - updateNote(data1.storage, data2.key, input4) + updateNote(data1.storage, data2.key, input4), + updateNote(data1.storage, data3.key, input6) ]) }) }) .then(function assert (data) { const data1 = data[0] const data2 = data[1] + const data3 = data[2] const jsonData1 = CSON.readFileSync(path.join(storagePath, 'notes', data1.key + '.cson')) t.is(input3.title, data1.title) @@ -118,6 +139,16 @@ test.serial('Update a note', (t) => { t.is(input4.tags.length, jsonData2.tags.length) t.deepEqual(input4.linesHighlighted, data2.linesHighlighted) t.deepEqual(input4.linesHighlighted, jsonData2.linesHighlighted) + + const fileData3 = fs.readFileSync(path.join(storagePath, 'test', 'test' + '.txt'), 'utf8') + const jsonData3 = CSON.readFileSync(path.join(storagePath, 'notes', data3.key + '.cson')) + t.is(input6.title, data3.title) + t.is(input6.title, jsonData3.title) + t.is(input6.content, data3.content) + t.is(input6.content, jsonData3.content) + t.is(input6.tags.length, data3.tags.length) + t.is(input6.tags.length, data3.tags.length) + t.is(fileData3, data3.content) }) }) diff --git a/yarn.lock b/yarn.lock index 604880e57..c6fce7496 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4337,6 +4337,11 @@ he@^1.1.1: version "1.1.1" resolved "http://registry.npm.taobao.org/he/download/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" +highlight.js@^9.13.1: + version "9.13.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" + integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A== + highlight.js@^9.3.0: version "9.12.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" @@ -4652,6 +4657,11 @@ invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2: dependencies: loose-envify "^1.0.0" +invert-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-color/-/invert-color-2.0.0.tgz#894ab1f7494a6e45f5e74c2f99ec042cd67dd23e" + integrity sha512-9s6IATlhOAr0/0MPUpLdMpk81ixIu8IqwPwORssXBauFT/4ff/iyEOcojd0UYuPwkDbJvL1+blIZGhqVIaAm5Q== + invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -6316,9 +6326,10 @@ mousetrap-global-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz#cd7de9222bd0646fa2e010d54c84a74c26a88edd" -mousetrap@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9" +mousetrap@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.2.tgz#caadd9cf886db0986fb2fee59a82f6bd37527587" + integrity sha512-jDjhi7wlHwdO6q6DS7YRmSHcuI+RVxadBkLt3KHrhd3C2b+w5pKefg3oj5beTcHZyVFA9Aksf+yEE1y5jxUjVA== ms@2.0.0: version "2.0.0"