diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 7f6b4bbd..304171ba 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -604,7 +604,10 @@ export default class CodeEditor extends React.Component { body, 'text/html' ) - const linkWithTitle = `[${parsedBody.title}](${pastedTxt})` + const escapePipe = (str) => { + return str.replace('|', '\\|') + } + const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})` resolve(linkWithTitle) } catch (e) { reject(e) diff --git a/browser/lib/findNoteTitle.js b/browser/lib/findNoteTitle.js index 81c9400f..b954f172 100644 --- a/browser/lib/findNoteTitle.js +++ b/browser/lib/findNoteTitle.js @@ -3,6 +3,17 @@ export function findNoteTitle (value) { let title = null let isInsideCodeBlock = false + if (splitted[0] === '---') { + let line = 0 + while (++line < splitted.length) { + if (splitted[line] === '---') { + splitted.splice(0, line + 1) + + break + } + } + } + splitted.some((line, index) => { const trimmedLine = line.trim() const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim() diff --git a/browser/lib/markdown-it-frontmatter.js b/browser/lib/markdown-it-frontmatter.js new file mode 100644 index 00000000..66d8ce89 --- /dev/null +++ b/browser/lib/markdown-it-frontmatter.js @@ -0,0 +1,24 @@ +'use strict' + +module.exports = function frontMatterPlugin (md) { + function frontmatter (state, startLine, endLine, silent) { + if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') { + return false + } + + let line = 0 + while (++line < state.lineMax) { + if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') { + state.line = line + 1 + + return true + } + } + + return false + } + + md.block.ruler.before('table', 'frontmatter', frontmatter, { + alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] + }) +} diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js new file mode 100644 index 00000000..716be83a --- /dev/null +++ b/browser/lib/markdown-toc-generator.js @@ -0,0 +1,100 @@ +/** + * @fileoverview Markdown table of contents generator + */ + +import toc from 'markdown-toc' +import diacritics from 'diacritics-map' +import stripColor from 'strip-color' + +const EOL = require('os').EOL + +/** + * @caseSensitiveSlugify Custom slugify function + * Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js), + * but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067 + */ +function caseSensitiveSlugify (str) { + function replaceDiacritics (str) { + return str.replace(/[À-ž]/g, function (ch) { + return diacritics[ch] || ch + }) + } + + function getTitle (str) { + if (/^\[[^\]]+\]\(/.test(str)) { + var m = /^\[([^\]]+)\]/.exec(str) + if (m) return m[1] + } + return str + } + + str = getTitle(str) + str = stripColor(str) + // str = str.toLowerCase() //let's be case sensitive + + // `.split()` is often (but not always) faster than `.replace()` + str = str.split(' ').join('-') + str = str.split(/\t/).join('--') + str = str.split(/<\/?[^>]+>/).join('') + str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('') + str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('') + str = replaceDiacritics(str) + return str +} + +const TOC_MARKER_START = '' +const TOC_MARKER_END = '' + +/** + * Takes care of proper updating given editor with TOC. + * If TOC doesn't exit in the editor, it's inserted at current caret position. + * Otherwise,TOC is updated in place. + * @param editor CodeMirror editor to be updated with TOC + */ +export function generateInEditor (editor) { + const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`) + + function tocExistsInEditor () { + return tocRegex.test(editor.getValue()) + } + + function updateExistingToc () { + const toc = generate(editor.getValue()) + const search = editor.getSearchCursor(tocRegex) + while (search.findNext()) { + search.replace(toc) + } + } + + function addTocAtCursorPosition () { + const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity})) + editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor()) + } + + if (tocExistsInEditor()) { + updateExistingToc() + } else { + addTocAtCursorPosition() + } +} + +/** + * Generates MD TOC based on MD document passed as string. + * @param markdownText MD document + * @returns generatedTOC String containing generated TOC + */ +export function generate (markdownText) { + const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify}) + return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END +} + +function wrapTocWithEol (toc, editor) { + const leftWrap = editor.getCursor().ch === 0 ? '' : EOL + const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL + return leftWrap + toc + rightWrap +} + +export default { + generate, + generateInEditor +} diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 49fd2f86..ba57ec6b 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -149,6 +149,7 @@ class Markdown { }) this.md.use(require('markdown-it-kbd')) this.md.use(require('markdown-it-admonition')) + this.md.use(require('./markdown-it-frontmatter')) const deflate = require('markdown-it-plantuml/lib/deflate') this.md.use(require('markdown-it-plantuml'), '', { diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js new file mode 100644 index 00000000..bed69735 --- /dev/null +++ b/browser/lib/newNote.js @@ -0,0 +1,62 @@ +import { hashHistory } from 'react-router' +import dataApi from 'browser/main/lib/dataApi' +import ee from 'browser/main/lib/eventEmitter' +import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' + +export function createMarkdownNote (storage, folder, dispatch, location) { + AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN') + AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') + return dataApi + .createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: '' + }) + .then(note => { + const noteHash = note.key + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + + hashHistory.push({ + pathname: location.pathname, + query: { key: noteHash } + }) + ee.emit('list:jump', noteHash) + ee.emit('detail:focus') + }) +} + +export function createSnippetNote (storage, folder, dispatch, location, config) { + AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET') + AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') + return dataApi + .createNote(storage, { + type: 'SNIPPET_NOTE', + folder: folder, + title: '', + description: '', + snippets: [ + { + name: '', + mode: config.editor.snippetDefaultLanguage || 'text', + content: '' + } + ] + }) + .then(note => { + const noteHash = note.key + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + hashHistory.push({ + pathname: location.pathname, + query: { key: noteHash } + }) + ee.emit('list:jump', noteHash) + ee.emit('detail:focus') + }) +} diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 1e30d28b..4e6d057a 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -29,6 +29,7 @@ import { formatDate } from 'browser/lib/date-formatter' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import striptags from 'striptags' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' +import markdownToc from 'browser/lib/markdown-toc-generator' class MarkdownNoteDetail extends React.Component { constructor (props) { @@ -47,6 +48,7 @@ class MarkdownNoteDetail extends React.Component { this.dispatchTimer = null this.toggleLockButton = this.handleToggleLockButton.bind(this) + this.generateToc = () => this.handleGenerateToc() } focus () { @@ -59,6 +61,7 @@ class MarkdownNoteDetail extends React.Component { const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT' this.handleSwitchMode(reversedType) }) + ee.on('code:generate-toc', this.generateToc) } componentWillReceiveProps (nextProps) { @@ -75,6 +78,7 @@ class MarkdownNoteDetail extends React.Component { componentWillUnmount () { ee.off('topbar:togglelockbutton', this.toggleLockButton) + ee.off('code:generate-toc', this.generateToc) if (this.saveQueue != null) this.saveNow() } @@ -262,6 +266,11 @@ class MarkdownNoteDetail extends React.Component { } } + handleGenerateToc () { + const editor = this.refs.content.refs.code.editor + markdownToc.generateInEditor(editor) + } + handleFocus (e) { this.focus() } @@ -365,6 +374,7 @@ class MarkdownNoteDetail extends React.Component { value={this.state.note.tags} saveTagsAlphabetically={config.ui.saveTagsAlphabetically} showTagsAlphabetically={config.ui.showTagsAlphabetically} + data={data} onChange={this.handleUpdateTag.bind(this)} /> diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl index 8d454203..7166a497 100644 --- a/browser/main/Detail/NoteDetailInfo.styl +++ b/browser/main/Detail/NoteDetailInfo.styl @@ -13,6 +13,7 @@ $info-margin-under-border = 30px display flex align-items center padding 0 20px + z-index 99 .info-left padding 0 10px diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 39d66eba..5bd45861 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -29,6 +29,7 @@ import InfoPanelTrashed from './InfoPanelTrashed' import { formatDate } from 'browser/lib/date-formatter' import i18n from 'browser/lib/i18n' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' +import markdownToc from 'browser/lib/markdown-toc-generator' const electron = require('electron') const { remote } = electron @@ -52,6 +53,7 @@ class SnippetNoteDetail extends React.Component { } this.scrollToNextTabThreshold = 0.7 + this.generateToc = () => this.handleGenerateToc() } componentDidMount () { @@ -65,6 +67,7 @@ class SnippetNoteDetail extends React.Component { enableLeftArrow: allTabs.offsetLeft !== 0 }) } + ee.on('code:generate-toc', this.generateToc) } componentWillReceiveProps (nextProps) { @@ -91,6 +94,16 @@ class SnippetNoteDetail extends React.Component { componentWillUnmount () { if (this.saveQueue != null) this.saveNow() + ee.off('code:generate-toc', this.generateToc) + } + + handleGenerateToc () { + const { note, snippetIndex } = this.state + const currentMode = note.snippets[snippetIndex].mode + if (currentMode.includes('Markdown')) { + const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor + markdownToc.generateInEditor(currentEditor) + } } handleChange (e) { @@ -441,7 +454,7 @@ class SnippetNoteDetail extends React.Component { const isSuper = global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey - if (isSuper && !e.shiftKey) { + if (isSuper && !e.shiftKey && !e.altKey) { e.preventDefault() this.addSnippet() } diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index ecaebf2e..ffa98786 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -6,71 +6,33 @@ import _ from 'lodash' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import i18n from 'browser/lib/i18n' import ee from 'browser/main/lib/eventEmitter' +import Autosuggest from 'react-autosuggest' class TagSelect extends React.Component { constructor (props) { super(props) this.state = { - newTag: '' + newTag: '', + suggestions: [] } - this.addtagHandler = this.handleAddTag.bind(this) + + this.handleAddTag = this.handleAddTag.bind(this) + this.onInputBlur = this.onInputBlur.bind(this) + this.onInputChange = this.onInputChange.bind(this) + this.onInputKeyDown = this.onInputKeyDown.bind(this) + this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this) + this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this) + this.onSuggestionSelected = this.onSuggestionSelected.bind(this) } - componentDidMount () { - this.value = this.props.value - ee.on('editor:add-tag', this.addtagHandler) - } - - componentDidUpdate () { - this.value = this.props.value - } - - componentWillUnmount () { - ee.off('editor:add-tag', this.addtagHandler) - } - - handleAddTag () { - this.refs.newTag.focus() - } - - handleNewTagInputKeyDown (e) { - switch (e.keyCode) { - case 9: - e.preventDefault() - this.submitTag() - break - case 13: - this.submitTag() - break - case 8: - if (this.refs.newTag.value.length === 0) { - this.removeLastTag() - } - } - } - - handleNewTagBlur (e) { - this.submitTag() - } - - removeLastTag () { - this.removeTagByCallback((value) => { - value.pop() - }) - } - - reset () { - this.setState({ - newTag: '' - }) - } - - submitTag () { + addNewTag (newTag) { AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG') - let { value } = this.props - let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_') - newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag + + newTag = newTag.trim().replace(/ +/g, '_') + if (newTag.charAt(0) === '#') { + newTag.substring(1) + } if (newTag.length <= 0) { this.setState({ @@ -79,17 +41,12 @@ class TagSelect extends React.Component { return } + let { value } = this.props value = _.isArray(value) ? value.slice() : [] - - if (!_.includes(value, newTag)) { - value.push(newTag) - } - - if (this.props.saveTagsAlphabetically) { - value = _.sortBy(value) - } + value.push(newTag) + value = _.uniq(value) this.setState({ newTag: '' @@ -99,10 +56,36 @@ class TagSelect extends React.Component { }) } - handleNewTagInputChange (e) { - this.setState({ - newTag: this.refs.newTag.value - }) + buildSuggestions () { + this.suggestions = _.sortBy(this.props.data.tagNoteMap.map( + (tag, name) => ({ + name, + nameLC: name.toLowerCase(), + size: tag.size + }) + ).filter( + tag => tag.size > 0 + ), ['name']) + } + + componentDidMount () { + this.value = this.props.value + + this.buildSuggestions() + + ee.on('editor:add-tag', this.handleAddTag) + } + + componentDidUpdate () { + this.value = this.props.value + } + + componentWillUnmount () { + ee.off('editor:add-tag', this.handleAddTag) + } + + handleAddTag () { + this.refs.newTag.input.focus() } handleTagRemoveButtonClick (tag) { @@ -111,6 +94,60 @@ class TagSelect extends React.Component { }, tag) } + onInputBlur (e) { + this.submitNewTag() + } + + onInputChange (e, { newValue, method }) { + this.setState({ + newTag: newValue + }) + } + + onInputKeyDown (e) { + switch (e.keyCode) { + case 9: + e.preventDefault() + this.submitNewTag() + break + case 13: + this.submitNewTag() + break + case 8: + if (this.state.newTag.length === 0) { + this.removeLastTag() + } + } + } + + onSuggestionsClearRequested () { + this.setState({ + suggestions: [] + }) + } + + onSuggestionsFetchRequested ({ value }) { + const valueLC = value.toLowerCase() + const suggestions = _.filter( + this.suggestions, + tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1 + ) + + this.setState({ + suggestions + }) + } + + onSuggestionSelected (event, { suggestion, suggestionValue }) { + this.addNewTag(suggestionValue) + } + + removeLastTag () { + this.removeTagByCallback((value) => { + value.pop() + }) + } + removeTagByCallback (callback, tag = null) { let { value } = this.props @@ -124,6 +161,18 @@ class TagSelect extends React.Component { this.props.onChange() } + reset () { + this.buildSuggestions() + + this.setState({ + newTag: '' + }) + } + + submitNewTag () { + this.addNewTag(this.refs.newTag.input.value) + } + render () { const { value, className, showTagsAlphabetically } = this.props @@ -144,6 +193,8 @@ class TagSelect extends React.Component { }) : [] + const { newTag, suggestions } = this.state + return (
{tagList} - this.handleNewTagInputChange(e)} - onKeyDown={(e) => this.handleNewTagInputKeyDown(e)} - onBlur={(e) => this.handleNewTagBlur(e)} + suggestions={suggestions} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSuggestionSelected} + getSuggestionValue={suggestion => suggestion.name} + renderSuggestion={suggestion => ( +
+ {suggestion.name} +
+ )} + inputProps={{ + placeholder: i18n.__('Add tag...'), + value: newTag, + onChange: this.onInputChange, + onKeyDown: this.onInputKeyDown, + onBlur: this.onInputBlur + }} />
) @@ -169,7 +232,6 @@ TagSelect.propTypes = { className: PropTypes.string, value: PropTypes.arrayOf(PropTypes.string), onChange: PropTypes.func - } export default CSSModules(TagSelect, styles) diff --git a/browser/main/Detail/TagSelect.styl b/browser/main/Detail/TagSelect.styl index 0ff4c6a3..7dc9dfe4 100644 --- a/browser/main/Detail/TagSelect.styl +++ b/browser/main/Detail/TagSelect.styl @@ -42,14 +42,6 @@ color: $ui-text-color padding 4px 16px 4px 8px -.newTag - box-sizing border-box - border none - background-color transparent - outline none - padding 0 4px - font-size 13px - body[data-theme="dark"] .tag background-color alpha($ui-dark-tag-backgroundColor, 60%) @@ -62,11 +54,6 @@ body[data-theme="dark"] .tag-label color $ui-dark-text-color - .newTag - border-color none - background-color transparent - color $ui-dark-text-color - body[data-theme="solarized-dark"] .tag background-color $ui-solarized-dark-tag-backgroundColor @@ -78,11 +65,6 @@ body[data-theme="solarized-dark"] .tag-label color $ui-solarized-dark-text-color - .newTag - border-color none - background-color transparent - color $ui-solarized-dark-text-color - body[data-theme="monokai"] .tag background-color $ui-monokai-button-backgroundColor @@ -92,9 +74,4 @@ body[data-theme="monokai"] background-color transparent .tag-label - color $ui-monokai-text-color - - .newTag - border-color none - background-color transparent - color $ui-monokai-text-color + color $ui-monokai-text-color \ No newline at end of file diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js index 85dc7f40..e739a550 100644 --- a/browser/main/NewNoteButton/index.js +++ b/browser/main/NewNoteButton/index.js @@ -7,6 +7,7 @@ import modal from 'browser/main/lib/modal' import NewNoteModal from 'browser/main/modals/NewNoteModal' import eventEmitter from 'browser/main/lib/eventEmitter' import i18n from 'browser/lib/i18n' +import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' const { remote } = require('electron') const { dialog } = remote @@ -37,13 +38,19 @@ class NewNoteButton extends React.Component { const { location, dispatch, config } = this.props const { storage, folder } = this.resolveTargetFolder() - modal.open(NewNoteModal, { - storage: storage.key, - folder: folder.key, - dispatch, - location, - config - }) + if (config.ui.defaultNote === 'MARKDOWN_NOTE') { + createMarkdownNote(storage.key, folder.key, dispatch, location) + } else if (config.ui.defaultNote === 'SNIPPET_NOTE') { + createSnippetNote(storage.key, folder.key, dispatch, location, config) + } else { + modal.open(NewNoteModal, { + storage: storage.key, + folder: folder.key, + dispatch, + location, + config + }) + } } resolveTargetFolder () { diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index 44da782b..1c8fe69a 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -80,6 +80,7 @@ class NoteList extends React.Component { this.getViewType = this.getViewType.bind(this) this.restoreNote = this.restoreNote.bind(this) this.copyNoteLink = this.copyNoteLink.bind(this) + this.navigate = this.navigate.bind(this) // TODO: not Selected noteKeys but SelectedNote(for reusing) this.state = { @@ -98,6 +99,7 @@ class NoteList extends React.Component { ee.on('list:isMarkdownNote', this.alertIfSnippetHandler) ee.on('import:file', this.importFromFileHandler) ee.on('list:jump', this.jumpNoteByHash) + ee.on('list:navigate', this.navigate) } componentWillReceiveProps (nextProps) { @@ -687,6 +689,16 @@ class NoteList extends React.Component { return copy(noteLink) } + navigate (sender, pathname) { + const { router } = this.context + router.push({ + pathname, + query: { + // key: noteKey + } + }) + } + save (note) { const { dispatch } = this.props dataApi diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index d72f0a8f..d17314b3 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -38,6 +38,22 @@ class StorageItem extends React.Component { { type: 'separator' }, + { + label: i18n.__('Export Storage'), + submenu: [ + { + label: i18n.__('Export as txt'), + click: (e) => this.handleExportStorageClick(e, 'txt') + }, + { + label: i18n.__('Export as md'), + click: (e) => this.handleExportStorageClick(e, 'md') + } + ] + }, + { + type: 'separator' + }, { label: i18n.__('Unlink Storage'), click: (e) => this.handleUnlinkStorageClick(e) @@ -68,6 +84,30 @@ class StorageItem extends React.Component { } } + handleExportStorageClick (e, fileType) { + const options = { + properties: ['openDirectory', 'createDirectory'], + buttonLabel: i18n.__('Select directory'), + title: i18n.__('Select a folder to export the files to'), + multiSelections: false + } + dialog.showOpenDialog(remote.getCurrentWindow(), options, + (paths) => { + if (paths && paths.length === 1) { + const { storage, dispatch } = this.props + dataApi + .exportStorage(storage.key, fileType, paths[0]) + .then(data => { + dispatch({ + type: 'EXPORT_STORAGE', + storage: data.storage, + fileType: data.fileType + }) + }) + } + }) + } + handleToggleButtonClick (e) { const { storage, dispatch } = this.props const isOpen = !this.state.isOpen diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 65f76691..86e148c8 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -210,12 +210,12 @@ class SideNav extends React.Component { const tags = pathSegments[pathSegments.length - 1] return (tags === 'alltags') ? [] - : tags.split(' ') + : tags.split(' ').map(tag => decodeURIComponent(tag)) } handleClickTagListItem (name) { const { router } = this.context - router.push(`/tags/${name}`) + router.push(`/tags/${encodeURIComponent(name)}`) } handleSortTagsByChange (e) { @@ -242,7 +242,7 @@ class SideNav extends React.Component { } else { listOfTags.push(tag) } - router.push(`/tags/${listOfTags.join(' ')}`) + router.push(`/tags/${listOfTags.map(tag => encodeURIComponent(tag)).join(' ')}`) } emptyTrash (entries) { diff --git a/browser/main/global.styl b/browser/main/global.styl index e4505a4e..815cff4e 100644 --- a/browser/main/global.styl +++ b/browser/main/global.styl @@ -132,6 +132,15 @@ body[data-theme="dark"] .CodeMirror-foldgutter-folded:after content: "\25B8" +.CodeMirror-hover + padding 2px 4px 0 4px + position absolute + z-index 99 + +.CodeMirror-hyperlink + cursor pointer + + .sortableItemHelper z-index modalZIndex + 5 @@ -156,3 +165,5 @@ body[data-theme="monokai"] body[data-theme="default"] .SideNav ::-webkit-scrollbar-thumb background-color rgba(255, 255, 255, 0.3) + +@import '../styles/Detail/TagSelect.styl' \ No newline at end of file diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js new file mode 100644 index 00000000..ce2c4573 --- /dev/null +++ b/browser/main/lib/dataApi/exportStorage.js @@ -0,0 +1,63 @@ +import { findStorage } from 'browser/lib/findStorage' +import resolveStorageData from './resolveStorageData' +import resolveStorageNotes from './resolveStorageNotes' +import filenamify from 'filenamify' +import * as path from 'path' +import * as fs from 'fs' + +/** + * @param {String} storageKey + * @param {String} fileType + * @param {String} exportDir + * + * @return {Object} + * ``` + * { + * storage: Object, + * fileType: String, + * exportDir: String + * } + * ``` + */ + +function exportStorage (storageKey, fileType, exportDir) { + let targetStorage + try { + targetStorage = findStorage(storageKey) + } catch (e) { + return Promise.reject(e) + } + + return resolveStorageData(targetStorage) + .then(storage => ( + resolveStorageNotes(storage).then(notes => ({storage, notes})) + )) + .then(function exportNotes (data) { + const { storage, notes } = data + const folderNamesMapping = {} + storage.folders.forEach(folder => { + const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'})) + folderNamesMapping[folder.key] = folderExportedDir + // make sure directory exists + try { + fs.mkdirSync(folderExportedDir) + } catch (e) {} + }) + notes + .filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE') + .forEach(markdownNote => { + const folderExportedDir = folderNamesMapping[markdownNote.folder] + const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}` + const notePath = path.join(folderExportedDir, snippetName) + fs.writeFileSync(notePath, markdownNote.content) + }) + + return { + storage, + fileType, + exportDir + } + }) +} + +module.exports = exportStorage diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 4e2f0061..92be6b93 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -9,6 +9,7 @@ const dataApi = { deleteFolder: require('./deleteFolder'), reorderFolder: require('./reorderFolder'), exportFolder: require('./exportFolder'), + exportStorage: require('./exportStorage'), createNote: require('./createNote'), updateNote: require('./updateNote'), deleteNote: require('./deleteNote'), diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index f6aa2c67..8b16f2a2 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -1,12 +1,9 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './NewNoteModal.styl' -import dataApi from 'browser/main/lib/dataApi' -import { hashHistory } from 'react-router' -import ee from 'browser/main/lib/eventEmitter' import ModalEscButton from 'browser/components/ModalEscButton' -import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import i18n from 'browser/lib/i18n' +import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' class NewNoteModal extends React.Component { constructor (props) { @@ -24,31 +21,10 @@ class NewNoteModal extends React.Component { } handleMarkdownNoteButtonClick (e) { - AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN') - AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') const { storage, folder, dispatch, location } = this.props - dataApi - .createNote(storage, { - type: 'MARKDOWN_NOTE', - folder: folder, - title: '', - content: '' - }) - .then(note => { - const noteHash = note.key - dispatch({ - type: 'UPDATE_NOTE', - note: note - }) - - hashHistory.push({ - pathname: location.pathname, - query: { key: noteHash } - }) - ee.emit('list:jump', noteHash) - ee.emit('detail:focus') - setTimeout(this.props.close, 200) - }) + createMarkdownNote(storage, folder, dispatch, location).then(() => { + setTimeout(this.props.close, 200) + }) } handleMarkdownNoteButtonKeyDown (e) { @@ -59,38 +35,10 @@ class NewNoteModal extends React.Component { } handleSnippetNoteButtonClick (e) { - AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET') - AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') const { storage, folder, dispatch, location, config } = this.props - - dataApi - .createNote(storage, { - type: 'SNIPPET_NOTE', - folder: folder, - title: '', - description: '', - snippets: [ - { - name: '', - mode: config.editor.snippetDefaultLanguage || 'text', - content: '' - } - ] - }) - .then(note => { - const noteHash = note.key - dispatch({ - type: 'UPDATE_NOTE', - note: note - }) - hashHistory.push({ - pathname: location.pathname, - query: { key: noteHash } - }) - ee.emit('list:jump', noteHash) - ee.emit('detail:focus') - setTimeout(this.props.close, 200) - }) + createSnippetNote(storage, folder, dispatch, location, config).then(() => { + setTimeout(this.props.close, 200) + }) } handleSnippetNoteButtonKeyDown (e) { diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 2d564042..00a79958 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -67,6 +67,7 @@ class UiTab extends React.Component { ui: { theme: this.refs.uiTheme.value, language: this.refs.uiLanguage.value, + defaultNote: this.refs.defaultNote.value, showCopyNotification: this.refs.showCopyNotification.checked, confirmDeletion: this.refs.confirmDeletion.checked, showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked, @@ -210,6 +211,22 @@ class UiTab extends React.Component { +
+
+ {i18n.__('Default New Note')} +
+
+ +
+
+