Skip to content

Commit 8b13ec4

Browse files
AWolf81khaliqgant
andauthored
Issue 1706 tag rename - finishing #2989 (#3469)
* allow a tag to be renamed and update all notes that use that tag • repurpose RenameFolderModal.styl to RenameModal so it is more generic * call handleConfirmButtonClick directly instead of sending through a confirm method * better name for method to confirm the rename * use close prop instead of a new method * use callback ref instead of legacy string refs * bind the handleChange in the constructor to allow for direct function assignment * update the tag in the URL upon change * use the eventEmitter to update the tags in the SnippetNoteDetail header via the TagSelect component * respect themes when modal is opened * show error message when trying to rename to an existing tag * lint fix, const over let * add missing letter * fix routing and add merge warning dialog * fix space-before-parens lint error * change theming * add check if tag changed Co-authored-by: Khaliq Gant <[email protected]>
1 parent 7fef766 commit 8b13ec4

File tree

5 files changed

+232
-2
lines changed

5 files changed

+232
-2
lines changed

browser/main/Detail/TagSelect.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class TagSelect extends React.Component {
2020
}
2121

2222
this.handleAddTag = this.handleAddTag.bind(this)
23+
this.handleRenameTag = this.handleRenameTag.bind(this)
2324
this.onInputBlur = this.onInputBlur.bind(this)
2425
this.onInputChange = this.onInputChange.bind(this)
2526
this.onInputKeyDown = this.onInputKeyDown.bind(this)
@@ -88,6 +89,7 @@ class TagSelect extends React.Component {
8889
this.buildSuggestions()
8990

9091
ee.on('editor:add-tag', this.handleAddTag)
92+
ee.on('sidebar:rename-tag', this.handleRenameTag)
9193
}
9294

9395
componentDidUpdate() {
@@ -96,12 +98,23 @@ class TagSelect extends React.Component {
9698

9799
componentWillUnmount() {
98100
ee.off('editor:add-tag', this.handleAddTag)
101+
ee.off('sidebar:rename-tag', this.handleRenameTag)
99102
}
100103

101104
handleAddTag() {
102105
this.refs.newTag.input.focus()
103106
}
104107

108+
handleRenameTag(event, tagChange) {
109+
const { value } = this.props
110+
const { tag, updatedTag } = tagChange
111+
const newTags = value.slice()
112+
113+
newTags[value.indexOf(tag)] = updatedTag
114+
this.value = newTags
115+
this.props.onChange()
116+
}
117+
105118
handleTagLabelClick(tag) {
106119
const { dispatch } = this.props
107120

browser/main/SideNav/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import dataApi from 'browser/main/lib/dataApi'
66
import styles from './SideNav.styl'
77
import { openModal } from 'browser/main/lib/modal'
88
import PreferencesModal from '../modals/PreferencesModal'
9+
import RenameTagModal from 'browser/main/modals/RenameTagModal'
910
import ConfigManager from 'browser/main/lib/ConfigManager'
1011
import StorageItem from './StorageItem'
1112
import TagListItem from 'browser/components/TagListItem'
@@ -170,6 +171,11 @@ class SideNav extends React.Component {
170171
)
171172
})
172173

174+
menu.push({
175+
label: i18n.__('Rename Tag'),
176+
click: this.handleRenameTagClick.bind(this, tag)
177+
})
178+
173179
context.popup(menu)
174180
}
175181

@@ -193,6 +199,16 @@ class SideNav extends React.Component {
193199
})
194200
}
195201

202+
handleRenameTagClick(tagName) {
203+
const { data, dispatch } = this.props
204+
205+
openModal(RenameTagModal, {
206+
tagName,
207+
data,
208+
dispatch
209+
})
210+
}
211+
196212
handleColorPickerConfirm(color) {
197213
const {
198214
dispatch,

browser/main/modals/RenameFolderModal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import PropTypes from 'prop-types'
22
import React from 'react'
33
import CSSModules from 'browser/lib/CSSModules'
4-
import styles from './RenameFolderModal.styl'
4+
import styles from './RenameModal.styl'
55
import dataApi from 'browser/main/lib/dataApi'
66
import { store } from 'browser/main/store'
77
import ModalEscButton from 'browser/components/ModalEscButton'

browser/main/modals/RenameFolderModal.styl renamed to browser/main/modals/RenameModal.styl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,18 @@
4646
font-size 14px
4747
colorPrimaryButton()
4848

49+
.error
50+
text-align center
51+
color #F44336
52+
height 20px
53+
4954
apply-theme(theme)
5055
body[data-theme={theme}]
5156
.root
5257
background-color transparent
5358

5459
.header
55-
background-color get-theme-var(theme, 'button--hover-backgroundColor')
60+
background-color transparent
5661
border-color get-theme-var(theme, 'borderColor')
5762
color get-theme-var(theme, 'text-color')
5863

browser/main/modals/RenameTagModal.js

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import PropTypes from 'prop-types'
2+
import React from 'react'
3+
import CSSModules from 'browser/lib/CSSModules'
4+
import styles from './RenameModal.styl'
5+
import dataApi from 'browser/main/lib/dataApi'
6+
import ModalEscButton from 'browser/components/ModalEscButton'
7+
import i18n from 'browser/lib/i18n'
8+
import { replace } from 'connected-react-router'
9+
import ee from 'browser/main/lib/eventEmitter'
10+
import { isEmpty } from 'lodash'
11+
import electron from 'electron'
12+
13+
const { remote } = electron
14+
const { dialog } = remote
15+
16+
class RenameTagModal extends React.Component {
17+
constructor(props) {
18+
super(props)
19+
20+
this.nameInput = null
21+
22+
this.handleChange = this.handleChange.bind(this)
23+
24+
this.setTextInputRef = el => {
25+
this.nameInput = el
26+
}
27+
28+
this.state = {
29+
name: props.tagName,
30+
oldName: props.tagName
31+
}
32+
}
33+
34+
componentDidMount() {
35+
this.nameInput.focus()
36+
this.nameInput.select()
37+
}
38+
39+
handleChange(e) {
40+
this.setState({
41+
name: this.nameInput.value,
42+
showerror: false,
43+
errormessage: ''
44+
})
45+
}
46+
47+
handleKeyDown(e) {
48+
if (e.keyCode === 27) {
49+
this.props.close()
50+
}
51+
}
52+
53+
handleInputKeyDown(e) {
54+
switch (e.keyCode) {
55+
case 13:
56+
this.handleConfirm()
57+
}
58+
}
59+
60+
handleConfirm() {
61+
if (this.state.name.trim().length > 0) {
62+
const { name, oldName } = this.state
63+
this.renameTag(oldName, name)
64+
}
65+
}
66+
67+
showError(message) {
68+
this.setState({
69+
showerror: true,
70+
errormessage: message
71+
})
72+
}
73+
74+
renameTag(tag, updatedTag) {
75+
const { data, dispatch } = this.props
76+
77+
if (tag === updatedTag) {
78+
// confirm with-out any change - just dismiss the modal
79+
this.props.close()
80+
return
81+
}
82+
83+
if (
84+
data.noteMap
85+
.map(note => note)
86+
.some(note => note.tags.indexOf(updatedTag) !== -1)
87+
) {
88+
const alertConfig = {
89+
type: 'warning',
90+
message: i18n.__('Confirm tag merge'),
91+
detail: i18n.__(
92+
`Tag ${tag} will be merged with existing tag ${updatedTag}`
93+
),
94+
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
95+
}
96+
97+
const dialogButtonIndex = dialog.showMessageBox(
98+
remote.getCurrentWindow(),
99+
alertConfig
100+
)
101+
102+
if (dialogButtonIndex === 1) {
103+
return // bail early on cancel click
104+
}
105+
}
106+
107+
const notes = data.noteMap
108+
.map(note => note)
109+
.filter(
110+
note => note.tags.indexOf(tag) !== -1 && note.tags.indexOf(updatedTag)
111+
)
112+
.map(note => {
113+
note = Object.assign({}, note)
114+
note.tags = note.tags.slice()
115+
116+
note.tags[note.tags.indexOf(tag)] = updatedTag
117+
118+
return note
119+
})
120+
121+
if (isEmpty(notes)) {
122+
this.showError(i18n.__('Tag exists'))
123+
124+
return
125+
}
126+
127+
Promise.all(
128+
notes.map(note => dataApi.updateNote(note.storage, note.key, note))
129+
)
130+
.then(updatedNotes => {
131+
updatedNotes.forEach(note => {
132+
dispatch({
133+
type: 'UPDATE_NOTE',
134+
note
135+
})
136+
})
137+
})
138+
.then(() => {
139+
if (window.location.hash.includes(tag)) {
140+
dispatch(replace(`/tags/${updatedTag}`))
141+
}
142+
ee.emit('sidebar:rename-tag', { tag, updatedTag })
143+
this.props.close()
144+
})
145+
}
146+
147+
render() {
148+
const { close } = this.props
149+
const { errormessage } = this.state
150+
151+
return (
152+
<div
153+
styleName='root'
154+
tabIndex='-1'
155+
onKeyDown={e => this.handleKeyDown(e)}
156+
>
157+
<div styleName='header'>
158+
<div styleName='title'>{i18n.__('Rename Tag')}</div>
159+
</div>
160+
<ModalEscButton handleEscButtonClick={close} />
161+
162+
<div styleName='control'>
163+
<input
164+
styleName='control-input'
165+
placeholder={i18n.__('Tag Name')}
166+
ref={this.setTextInputRef}
167+
value={this.state.name}
168+
onChange={this.handleChange}
169+
onKeyDown={e => this.handleInputKeyDown(e)}
170+
/>
171+
<button
172+
styleName='control-confirmButton'
173+
onClick={() => this.handleConfirm()}
174+
>
175+
{i18n.__('Confirm')}
176+
</button>
177+
</div>
178+
<div className='error' styleName='error'>
179+
{errormessage}
180+
</div>
181+
</div>
182+
)
183+
}
184+
}
185+
186+
RenameTagModal.propTypes = {
187+
storage: PropTypes.shape({
188+
key: PropTypes.string
189+
}),
190+
folder: PropTypes.shape({
191+
key: PropTypes.string,
192+
name: PropTypes.string
193+
})
194+
}
195+
196+
export default CSSModules(RenameTagModal, styles)

0 commit comments

Comments
 (0)