diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 463f0a16..17d2cb82 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -291,26 +291,7 @@ export default class MarkdownPreview extends React.Component { } handleSaveAsMd () { - this.exportAsDocument('md', (noteContent, exportTasks) => { - let result = noteContent - if (this.props && this.props.storagePath && this.props.noteKey) { - const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( - noteContent, - this.props.storagePath - ) - attachmentsAbsolutePaths.forEach(attachment => { - exportTasks.push({ - src: attachment, - dst: attachmentManagement.DESTINATION_FOLDER - }) - }) - result = attachmentManagement.removeStorageAndNoteReferences( - noteContent, - this.props.noteKey - ) - } - return result - }) + this.exportAsDocument('md') } handleSaveAsHtml () { @@ -339,11 +320,6 @@ export default class MarkdownPreview extends React.Component { ) let body = this.markdown.render(noteContent) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] - const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( - noteContent, - this.props.storagePath - ) - files.forEach(file => { if (global.process.platform === 'win32') { file = file.replace('file:///', '') @@ -355,16 +331,6 @@ export default class MarkdownPreview extends React.Component { dst: 'css' }) }) - attachmentsAbsolutePaths.forEach(attachment => { - exportTasks.push({ - src: attachment, - dst: attachmentManagement.DESTINATION_FOLDER - }) - }) - body = attachmentManagement.removeStorageAndNoteReferences( - body, - this.props.noteKey - ) let styles = '' files.forEach(file => { @@ -397,8 +363,9 @@ export default class MarkdownPreview extends React.Component { if (filename) { const content = this.props.value const storage = this.props.storagePath + const nodeKey = this.props.noteKey - exportNote(storage, content, filename, contentFormatter) + exportNote(nodeKey, storage, content, filename, contentFormatter) .then(res => { dialog.showMessageBox(remote.getCurrentWindow(), { type: 'info', diff --git a/browser/main/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js index 4ce610fa..15535186 100644 --- a/browser/main/Detail/InfoPanel.js +++ b/browser/main/Detail/InfoPanel.js @@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
- - - - diff --git a/browser/main/Detail/InfoPanelTrashed.js b/browser/main/Detail/InfoPanelTrashed.js index db64a284..d4c8045d 100644 --- a/browser/main/Detail/InfoPanelTrashed.js +++ b/browser/main/Detail/InfoPanelTrashed.js @@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
- - - diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 4a38ffe5..4a5076da 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -645,11 +645,18 @@ class SnippetNoteDetail extends React.Component { if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none' } - showWarning () { + showWarning (e, msg) { + const warningMessage = (msg) => ({ + 'export-txt': 'Text export', + 'export-md': 'Markdown export', + 'export-html': 'HTML export', + 'print': 'Print' + })[msg] + dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Sorry!'), - detail: i18n.__('md/text import is available only a markdown note.'), + detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'), buttons: [i18n.__('OK')] }) } @@ -800,7 +807,9 @@ class SnippetNoteDetail extends React.Component { createdAt={formatDate(note.createdAt)} exportAsMd={this.showWarning} exportAsTxt={this.showWarning} + exportAsHtml={this.showWarning} type={note.type} + print={this.showWarning} />
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index 08d7d2e2..d1c8d14a 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -64,13 +64,14 @@ class NoteList extends React.Component { this.focusHandler = () => { this.refs.list.focus() } - this.alertIfSnippetHandler = () => { - this.alertIfSnippet() + this.alertIfSnippetHandler = (event, msg) => { + this.alertIfSnippet(msg) } this.importFromFileHandler = this.importFromFile.bind(this) this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this) this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this) this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this) + this.cloneNote = this.cloneNote.bind(this) this.deleteNote = this.deleteNote.bind(this) this.focusNote = this.focusNote.bind(this) this.pinToTop = this.pinToTop.bind(this) @@ -96,6 +97,7 @@ class NoteList extends React.Component { this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000) ee.on('list:next', this.selectNextNoteHandler) ee.on('list:prior', this.selectPriorNoteHandler) + ee.on('list:clone', this.cloneNote) ee.on('list:focus', this.focusHandler) ee.on('list:isMarkdownNote', this.alertIfSnippetHandler) ee.on('import:file', this.importFromFileHandler) @@ -118,6 +120,7 @@ class NoteList extends React.Component { ee.off('list:next', this.selectNextNoteHandler) ee.off('list:prior', this.selectPriorNoteHandler) + ee.off('list:clone', this.cloneNote) ee.off('list:focus', this.focusHandler) ee.off('list:isMarkdownNote', this.alertIfSnippetHandler) ee.off('import:file', this.importFromFileHandler) @@ -277,12 +280,6 @@ class NoteList extends React.Component { ee.emit('top:new-note') } - // D key - if (e.keyCode === 68) { - e.preventDefault() - this.deleteNote() - } - // E key if (e.keyCode === 69) { e.preventDefault() @@ -495,14 +492,21 @@ class NoteList extends React.Component { }) } - alertIfSnippet () { + alertIfSnippet (msg) { + const warningMessage = (msg) => ({ + 'export-txt': 'Text export', + 'export-md': 'Markdown export', + 'export-html': 'HTML export', + 'print': 'Print' + })[msg] + const targetIndex = this.getTargetIndex() if (this.notes[targetIndex].type === 'SNIPPET_NOTE') { dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Sorry!'), - detail: i18n.__('md/text import is available only a markdown note.'), - buttons: [i18n.__('OK'), i18n.__('Cancel')] + detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'), + buttons: [i18n.__('OK')] }) } } diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index 7b5caf72..3b11e2f4 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -204,6 +204,20 @@ class StorageItem extends React.Component { folderKey: data.folderKey, fileType: data.fileType }) + return data + }) + .then(data => { + dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'info', + message: 'Exported to "' + data.exportDir + '"' + }) + }) + .catch(err => { + dialog.showErrorBox( + 'Export error', + err ? err.message || err : 'Unexpected error during export' + ) + throw err }) } }) diff --git a/browser/main/lib/dataApi/copyFile.js b/browser/main/lib/dataApi/copyFile.js index 2dc66309..6f23aae2 100755 --- a/browser/main/lib/dataApi/copyFile.js +++ b/browser/main/lib/dataApi/copyFile.js @@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) { const dstFolder = path.dirname(dstPath) if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder) - const input = fs.createReadStream(srcPath) + const input = fs.createReadStream(decodeURI(srcPath)) const output = fs.createWriteStream(dstPath) output.on('error', reject) diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js index 3e998f15..771f77dc 100644 --- a/browser/main/lib/dataApi/exportFolder.js +++ b/browser/main/lib/dataApi/exportFolder.js @@ -1,9 +1,9 @@ import { findStorage } from 'browser/lib/findStorage' import resolveStorageData from './resolveStorageData' import resolveStorageNotes from './resolveStorageNotes' +import exportNote from './exportNote' import filenamify from 'filenamify' import * as path from 'path' -import * as fs from 'fs' /** * @param {String} storageKey @@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) { notes .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') - .forEach(snippet => { - const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`) - fs.writeFileSync(notePath, snippet.content) + .forEach(note => { + const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`) + exportNote(note.key, storage.path, note.content, notePath, null) }) return { diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index e4fec5f4..b358e548 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage' const fs = require('fs') const path = require('path') +const attachmentManagement = require('./attachmentManagement') + /** - * Export note together with images + * Export note together with attachments * - * If images is stored in the storage, creates 'images' subfolder in target directory - * and copies images to it. Changes links to images in the content of the note + * If attachments are stored in the storage, creates 'attachments' subfolder in target directory + * and copies attachments to it. Changes links to images in the content of the note * + * @param {String} nodeKey key of the node that should be exported * @param {String} storageKey or storage path * @param {String} noteContent Content to export * @param {String} targetPath Path to exported file * @param {function} outputFormatter * @return {Promise.<*[]>} */ -function exportNote (storageKey, noteContent, targetPath, outputFormatter) { +function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) { const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path const exportTasks = [] if (!storagePath) { throw new Error('Storage path is not found') } + const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( + noteContent, + storagePath + ) + attachmentsAbsolutePaths.forEach(attachment => { + exportTasks.push({ + src: attachment, + dst: attachmentManagement.DESTINATION_FOLDER + }) + }) - let exportedData = noteContent + let exportedData = attachmentManagement.removeStorageAndNoteReferences( + noteContent, + nodeKey + ) if (outputFormatter) { exportedData = outputFormatter(exportedData, exportTasks) diff --git a/lib/main-menu.js b/lib/main-menu.js index 91f3c8c6..05921347 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -85,39 +85,24 @@ const file = { }, { label: 'Focus Note', - accelerator: 'Control+E', + accelerator: macOS ? 'Command+E' : 'Control+E', click () { mainWindow.webContents.send('detail:focus') } }, { - type: 'separator' + label: 'Delete Note', + accelerator: macOS ? 'Command+Shift+Backspace' : 'Control+Shift+Backspace', + click () { + mainWindow.webContents.send('detail:delete') + } }, { - label: 'Export as', - submenu: [ - { - label: 'Plain Text (.txt)', - click () { - mainWindow.webContents.send('list:isMarkdownNote') - mainWindow.webContents.send('export:save-text') - } - }, - { - label: 'MarkDown (.md)', - click () { - mainWindow.webContents.send('list:isMarkdownNote') - mainWindow.webContents.send('export:save-md') - } - }, - { - label: 'HTML (.html)', - click () { - mainWindow.webContents.send('list:isMarkdownNote') - mainWindow.webContents.send('export:save-html') - } - } - ] + label: 'Clone Note', + accelerator: macOS ? 'Command+D' : 'Control+D', + click () { + mainWindow.webContents.send('list:clone') + } }, { type: 'separator' @@ -134,13 +119,30 @@ const file = { ] }, { - type: 'separator' - }, - { - label: 'Format Table', - click () { - mainWindow.webContents.send('code:format-table') - } + label: 'Export as', + submenu: [ + { + label: 'Plain Text (.txt)', + click () { + mainWindow.webContents.send('list:isMarkdownNote', 'export-txt') + mainWindow.webContents.send('export:save-text') + } + }, + { + label: 'MarkDown (.md)', + click () { + mainWindow.webContents.send('list:isMarkdownNote', 'export-md') + mainWindow.webContents.send('export:save-md') + } + }, + { + label: 'HTML (.html)', + click () { + mainWindow.webContents.send('list:isMarkdownNote', 'export-html') + mainWindow.webContents.send('export:save-html') + } + } + ] }, { type: 'separator' @@ -153,24 +155,20 @@ const file = { } }, { - type: 'separator' - }, - { - label: 'Print', - accelerator: 'CommandOrControl+P', + label: 'Format Table', click () { - mainWindow.webContents.send('list:isMarkdownNote') - mainWindow.webContents.send('print') + mainWindow.webContents.send('code:format-table') } }, { type: 'separator' }, { - label: 'Delete Note', - accelerator: macOS ? 'Control+Backspace' : 'Control+Delete', + label: 'Print', + accelerator: 'CommandOrControl+P', click () { - mainWindow.webContents.send('detail:delete') + mainWindow.webContents.send('list:isMarkdownNote', 'print') + mainWindow.webContents.send('print') } } ] @@ -296,9 +294,6 @@ const view = { mainWindow.setFullScreen(!mainWindow.isFullScreen()) } }, - { - type: 'separator' - }, { label: 'Toggle Side Bar', accelerator: 'CommandOrControl+B', diff --git a/package.json b/package.json index 1fb8e380..128a21af 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "escape-string-regexp": "^1.0.5", "file-uri-to-path": "^1.0.0", "file-url": "^2.0.2", - "filenamify": "^2.0.0", + "filenamify": "^2.1.0", "flowchart.js": "^1.6.5", "font-awesome": "^4.3.0", "fs-extra": "^5.0.0", diff --git a/tests/dataApi/copyFile-test.js b/tests/dataApi/copyFile-test.js new file mode 100644 index 00000000..412d510a --- /dev/null +++ b/tests/dataApi/copyFile-test.js @@ -0,0 +1,35 @@ +const test = require('ava') +const copyFile = require('browser/main/lib/dataApi/copyFile') + +const path = require('path') +const fs = require('fs') + +const testFile = 'test.txt' +const srcFolder = path.join(__dirname, '🤔') +const srcPath = path.join(srcFolder, testFile) +const dstFolder = path.join(__dirname, '😇') +const dstPath = path.join(dstFolder, testFile) + +test.before((t) => { + if (!fs.existsSync(srcFolder)) fs.mkdirSync(srcFolder) + + fs.writeFileSync(srcPath, 'test') +}) + +test('`copyFile` should handle encoded URI on src path', (t) => { + return copyFile(encodeURI(srcPath), dstPath) + .then(() => { + t.true(true) + }) + .catch(() => { + t.true(false) + }) +}) + +test.after((t) => { + fs.unlinkSync(srcPath) + fs.unlinkSync(dstPath) + fs.rmdirSync(srcFolder) + fs.rmdirSync(dstFolder) +}) + diff --git a/yarn.lock b/yarn.lock index 4b24d45d..48dd9056 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3591,9 +3591,9 @@ filename-reserved-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" -filenamify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-2.0.0.tgz#bd162262c0b6e94bfbcdcf19a3bbb3764f785695" +filenamify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-2.1.0.tgz#88faf495fb1b47abfd612300002a16228c677ee9" dependencies: filename-reserved-regex "^2.0.0" strip-outer "^1.0.0"