diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index 6e3e7a5b..e1d11e0d 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -20,6 +20,7 @@ class TagSelect extends React.Component { } this.handleAddTag = this.handleAddTag.bind(this) + this.handleRenameTag = this.handleRenameTag.bind(this) this.onInputBlur = this.onInputBlur.bind(this) this.onInputChange = this.onInputChange.bind(this) this.onInputKeyDown = this.onInputKeyDown.bind(this) @@ -88,6 +89,7 @@ class TagSelect extends React.Component { this.buildSuggestions() ee.on('editor:add-tag', this.handleAddTag) + ee.on('sidebar:rename-tag', this.handleRenameTag) } componentDidUpdate() { @@ -96,12 +98,23 @@ class TagSelect extends React.Component { componentWillUnmount() { ee.off('editor:add-tag', this.handleAddTag) + ee.off('sidebar:rename-tag', this.handleRenameTag) } handleAddTag() { this.refs.newTag.input.focus() } + handleRenameTag(event, tagChange) { + const { value } = this.props + const { tag, updatedTag } = tagChange + const newTags = value.slice() + + newTags[value.indexOf(tag)] = updatedTag + this.value = newTags + this.props.onChange() + } + handleTagLabelClick(tag) { const { dispatch } = this.props diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index cd11e652..c2f04116 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -6,6 +6,7 @@ import dataApi from 'browser/main/lib/dataApi' import styles from './SideNav.styl' import { openModal } from 'browser/main/lib/modal' import PreferencesModal from '../modals/PreferencesModal' +import RenameTagModal from 'browser/main/modals/RenameTagModal' import ConfigManager from 'browser/main/lib/ConfigManager' import StorageItem from './StorageItem' import TagListItem from 'browser/components/TagListItem' @@ -170,6 +171,11 @@ class SideNav extends React.Component { ) }) + menu.push({ + label: i18n.__('Rename Tag'), + click: this.handleRenameTagClick.bind(this, tag) + }) + context.popup(menu) } @@ -193,6 +199,16 @@ class SideNav extends React.Component { }) } + handleRenameTagClick(tagName) { + const { data, dispatch } = this.props + + openModal(RenameTagModal, { + tagName, + data, + dispatch + }) + } + handleColorPickerConfirm(color) { const { dispatch, diff --git a/browser/main/modals/RenameFolderModal.js b/browser/main/modals/RenameFolderModal.js index a8d6f386..93823e41 100644 --- a/browser/main/modals/RenameFolderModal.js +++ b/browser/main/modals/RenameFolderModal.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types' import React from 'react' import CSSModules from 'browser/lib/CSSModules' -import styles from './RenameFolderModal.styl' +import styles from './RenameModal.styl' import dataApi from 'browser/main/lib/dataApi' import { store } from 'browser/main/store' import ModalEscButton from 'browser/components/ModalEscButton' diff --git a/browser/main/modals/RenameFolderModal.styl b/browser/main/modals/RenameModal.styl similarity index 93% rename from browser/main/modals/RenameFolderModal.styl rename to browser/main/modals/RenameModal.styl index 435aa6a0..f1a37b33 100644 --- a/browser/main/modals/RenameFolderModal.styl +++ b/browser/main/modals/RenameModal.styl @@ -46,13 +46,18 @@ font-size 14px colorPrimaryButton() +.error + text-align center + color #F44336 + height 20px + apply-theme(theme) body[data-theme={theme}] .root background-color transparent .header - background-color get-theme-var(theme, 'button--hover-backgroundColor') + background-color transparent border-color get-theme-var(theme, 'borderColor') color get-theme-var(theme, 'text-color') diff --git a/browser/main/modals/RenameTagModal.js b/browser/main/modals/RenameTagModal.js new file mode 100644 index 00000000..8d2d0249 --- /dev/null +++ b/browser/main/modals/RenameTagModal.js @@ -0,0 +1,196 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './RenameModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import ModalEscButton from 'browser/components/ModalEscButton' +import i18n from 'browser/lib/i18n' +import { replace } from 'connected-react-router' +import ee from 'browser/main/lib/eventEmitter' +import { isEmpty } from 'lodash' +import electron from 'electron' + +const { remote } = electron +const { dialog } = remote + +class RenameTagModal extends React.Component { + constructor(props) { + super(props) + + this.nameInput = null + + this.handleChange = this.handleChange.bind(this) + + this.setTextInputRef = el => { + this.nameInput = el + } + + this.state = { + name: props.tagName, + oldName: props.tagName + } + } + + componentDidMount() { + this.nameInput.focus() + this.nameInput.select() + } + + handleChange(e) { + this.setState({ + name: this.nameInput.value, + showerror: false, + errormessage: '' + }) + } + + handleKeyDown(e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown(e) { + switch (e.keyCode) { + case 13: + this.handleConfirm() + } + } + + handleConfirm() { + if (this.state.name.trim().length > 0) { + const { name, oldName } = this.state + this.renameTag(oldName, name) + } + } + + showError(message) { + this.setState({ + showerror: true, + errormessage: message + }) + } + + renameTag(tag, updatedTag) { + const { data, dispatch } = this.props + + if (tag === updatedTag) { + // confirm with-out any change - just dismiss the modal + this.props.close() + return + } + + if ( + data.noteMap + .map(note => note) + .some(note => note.tags.indexOf(updatedTag) !== -1) + ) { + const alertConfig = { + type: 'warning', + message: i18n.__('Confirm tag merge'), + detail: i18n.__( + `Tag ${tag} will be merged with existing tag ${updatedTag}` + ), + buttons: [i18n.__('Confirm'), i18n.__('Cancel')] + } + + const dialogButtonIndex = dialog.showMessageBox( + remote.getCurrentWindow(), + alertConfig + ) + + if (dialogButtonIndex === 1) { + return // bail early on cancel click + } + } + + const notes = data.noteMap + .map(note => note) + .filter( + note => note.tags.indexOf(tag) !== -1 && note.tags.indexOf(updatedTag) + ) + .map(note => { + note = Object.assign({}, note) + note.tags = note.tags.slice() + + note.tags[note.tags.indexOf(tag)] = updatedTag + + return note + }) + + if (isEmpty(notes)) { + this.showError(i18n.__('Tag exists')) + + return + } + + Promise.all( + notes.map(note => dataApi.updateNote(note.storage, note.key, note)) + ) + .then(updatedNotes => { + updatedNotes.forEach(note => { + dispatch({ + type: 'UPDATE_NOTE', + note + }) + }) + }) + .then(() => { + if (window.location.hash.includes(tag)) { + dispatch(replace(`/tags/${updatedTag}`)) + } + ee.emit('sidebar:rename-tag', { tag, updatedTag }) + this.props.close() + }) + } + + render() { + const { close } = this.props + const { errormessage } = this.state + + return ( +