mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-03-08 14:03:46 +08:00
fix(*): can select participant with space if it's an email
Summary: Fixes T2272 If the label is too long, the unread icons will now ellipsis truncate the text correctly instead of being pushed off of the side. I made the max-width of the sidebar 17px wider to allow for Karim's french "Inbox" translation. Fixes T2266 Added some more protection to places where filenames could be blank when downloading. This was partially fixed by Rob's D1685 Fixes T2258 You can now finish selecting a participant by pressing `space`, but only when the participant looks like an email address. When you do this we directly add the email address to the chip instead of looking up a contact. This allows you to send just to the email instead of adding a potentially erroneous name into the input field Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Maniphest Tasks: T2272, T2266, T2258 Differential Revision: https://phab.nylas.com/D1729
This commit is contained in:
parent
ca95d03c5d
commit
93ea9c6165
11 changed files with 87 additions and 20 deletions
internal_packages
account-sidebar/lib
composer/lib
notifications/lib
spec-nylas
src
components
flux
static/mixins
|
@ -21,9 +21,9 @@ class AccountSidebarTagItem extends React.Component
|
|||
'selected': @props.select
|
||||
|
||||
<div className={coontainerClass} onClick={@_onClick} id={@props.item.id}>
|
||||
{unread}
|
||||
<RetinaImg name={"#{@props.item.id}.png"} fallback={'folder.png'} mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<span className="name"> {@props.item.name}</span>
|
||||
{unread}
|
||||
</div>
|
||||
|
||||
_onClick: (event) =>
|
||||
|
|
|
@ -12,7 +12,7 @@ class AccountSidebar extends React.Component
|
|||
@containerRequired: false
|
||||
@containerStyles:
|
||||
minWidth: 165
|
||||
maxWidth: 190
|
||||
maxWidth: 207
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
|
|
@ -77,13 +77,13 @@ class ParticipantsTextField extends React.Component
|
|||
false
|
||||
@props.change(updates)
|
||||
|
||||
_add: (values) =>
|
||||
_add: (values, options={}) =>
|
||||
# If the input is a string, parse out email addresses and build
|
||||
# an array of contact objects. For each email address wrapped in
|
||||
# parentheses, look for a preceding name, if one exists.
|
||||
|
||||
if _.isString(values)
|
||||
values = ContactStore.parseContactsInString(values)
|
||||
values = ContactStore.parseContactsInString(values, options)
|
||||
|
||||
# Safety check: remove anything from the incoming values that isn't
|
||||
# a Contact. We should never receive anything else in the values array.
|
||||
|
|
|
@ -14,7 +14,7 @@ class ActivitySidebar extends React.Component
|
|||
@containerRequired: false
|
||||
@containerStyles:
|
||||
minWidth: 165
|
||||
maxWidth: 190
|
||||
maxWidth: 207
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
|
|
@ -154,6 +154,27 @@ describe 'TokenizingTextField', ->
|
|||
ReactTestUtils.Simulate.mouseDown(React.findDOMNode(menuItem))
|
||||
expect(@propAdd).toHaveBeenCalledWith([participant4])
|
||||
|
||||
it "manually enters whatever's in the field when the user presses the space bar as long as it looks like an email", ->
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'abc@foo.com '}})
|
||||
advanceClock(10)
|
||||
expect(@propAdd).toHaveBeenCalledWith("abc@foo.com", skipNameLookup: true)
|
||||
|
||||
it "doesn't sumbmit if it looks like an email but has no space at the end", ->
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'abc@foo.com'}})
|
||||
advanceClock(10)
|
||||
expect(@propCompletionsForInput.calls[0].args[0]).toBe('abc@foo.com')
|
||||
expect(@propAdd).not.toHaveBeenCalled()
|
||||
|
||||
it "allows spaces if what's currently being entered doesn't look like an email", ->
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'ab'}})
|
||||
advanceClock(10)
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'ab '}})
|
||||
advanceClock(10)
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'ab c'}})
|
||||
advanceClock(10)
|
||||
expect(@propCompletionsForInput.calls[2].args[0]).toBe('ab c')
|
||||
expect(@propAdd).not.toHaveBeenCalled()
|
||||
|
||||
['enter', ','].forEach (key) ->
|
||||
describe "when the user presses #{key}", ->
|
||||
describe "and there is an completion available", ->
|
||||
|
@ -168,7 +189,7 @@ describe 'TokenizingTextField', ->
|
|||
@completions = []
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'abc'}})
|
||||
NylasTestUtils.keyPress(key, @renderedInput)
|
||||
expect(@propAdd).toHaveBeenCalledWith('abc')
|
||||
expect(@propAdd).toHaveBeenCalledWith('abc', {})
|
||||
|
||||
describe "when the user presses tab", ->
|
||||
describe "and there is an completion available", ->
|
||||
|
@ -183,7 +204,7 @@ describe 'TokenizingTextField', ->
|
|||
ReactTestUtils.Simulate.focus(@renderedInput)
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'text'}})
|
||||
ReactTestUtils.Simulate.blur(@renderedInput)
|
||||
expect(@propAdd).toHaveBeenCalledWith('text')
|
||||
expect(@propAdd).toHaveBeenCalledWith('text', {})
|
||||
|
||||
it 'should clear the entered text', ->
|
||||
ReactTestUtils.Simulate.focus(@renderedInput)
|
||||
|
|
|
@ -87,3 +87,22 @@ describe "ContactStore", ->
|
|||
it "can return more than 5 if requested", ->
|
||||
results = ContactStore.searchContacts("fi", limit: 6)
|
||||
expect(results.length).toBe 6
|
||||
|
||||
describe 'parseContactsInString', ->
|
||||
testCases =
|
||||
"evan@nylas.com": [new Contact(name: "evan@nylas.com", email: "evan@nylas.com")]
|
||||
"Evan Morikawa": []
|
||||
"Evan Morikawa <evan@nylas.com>": [new Contact(name: "Evan Morikawa", email: "evan@nylas.com")]
|
||||
"Evan Morikawa (evan@nylas.com)": [new Contact(name: "Evan Morikawa", email: "evan@nylas.com")]
|
||||
"Evan (evan@nylas.com)": [new Contact(name: "Evan", email: "evan@nylas.com")]
|
||||
"Evan Morikawa <evan@nylas.com>, Ben <ben@nylas.com>": [
|
||||
new Contact(name: "Evan Morikawa", email: "evan@nylas.com")
|
||||
new Contact(name: "Ben", email: "ben@nylas.com")
|
||||
]
|
||||
|
||||
_.forEach testCases, (value, key) ->
|
||||
it "works for #{key}", ->
|
||||
testContacts = ContactStore.parseContactsInString(key).map (c) -> c.nameEmail()
|
||||
expectedContacts = value.map (c) -> c.nameEmail()
|
||||
expect(testContacts).toEqual expectedContacts
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ React = require 'react/addons'
|
|||
classNames = require 'classnames'
|
||||
_ = require 'underscore'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
{Contact, ContactStore} = require 'nylas-exports'
|
||||
{Utils, Contact, ContactStore} = require 'nylas-exports'
|
||||
RetinaImg = require './retina-img'
|
||||
|
||||
{DragDropMixin} = require 'react-dnd'
|
||||
|
@ -303,7 +303,13 @@ TokenizingTextField = React.createClass
|
|||
@setState
|
||||
selectedTokenKey: null
|
||||
inputValue: val
|
||||
@_refreshCompletions(val)
|
||||
|
||||
# If it looks like an email, and the last character entered was a
|
||||
# space, then let's add the input value.
|
||||
if Utils.emailRegex().test(val) and _.last(val) is " "
|
||||
@_addInputValue(val[0...-1], skipNameLookup: true)
|
||||
else
|
||||
@_refreshCompletions(val)
|
||||
|
||||
_onInputBlurred: ->
|
||||
if @props.clearOnBlur
|
||||
|
@ -324,10 +330,10 @@ TokenizingTextField = React.createClass
|
|||
|
||||
# Managing Tokens
|
||||
|
||||
_addInputValue: (input) ->
|
||||
_addInputValue: (input, options={}) ->
|
||||
return if @_atMaxTokens()
|
||||
input ?= @state.inputValue
|
||||
@props.onAdd(input)
|
||||
@props.onAdd(input, options)
|
||||
@_clearInput()
|
||||
|
||||
_selectToken: (token) ->
|
||||
|
|
|
@ -275,7 +275,14 @@ Utils =
|
|||
toMatch = domains[0]
|
||||
return _.every(domains, (domain) -> domain.length > 0 and toMatch is domain)
|
||||
|
||||
emailRegex: /[a-z.A-Z0-9%+_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/g
|
||||
# It's important that the regex be wrapped in parens, otherwise
|
||||
# javascript's RegExp::exec method won't find anything even when the
|
||||
# regex matches!
|
||||
#
|
||||
# It's also imporant we return a fresh copy of the RegExp every time. A
|
||||
# javascript regex is stateful and multiple functions using this method
|
||||
# will cause unexpected behavior!
|
||||
emailRegex: -> new RegExp(/([a-z.A-Z0-9%+_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4})/g)
|
||||
|
||||
emailHasCommonDomain: (email="") ->
|
||||
domain = _.last(email.toLowerCase().trim().split("@"))
|
||||
|
|
|
@ -82,23 +82,24 @@ class ContactStore
|
|||
|
||||
matches
|
||||
|
||||
parseContactsInString: (str) =>
|
||||
parseContactsInString: (contactString, {skipNameLookup}={}) =>
|
||||
detected = []
|
||||
while (match = Utils.emailRegex.exec(str))
|
||||
emailRegex = new RegExp(Utils.emailRegex())
|
||||
while (match = emailRegex.exec(contactString))
|
||||
email = match[0]
|
||||
name = null
|
||||
|
||||
hasLeadingParen = str[match.index-1] in ['(','<']
|
||||
hasTrailingParen = str[match.index+email.length] in [')','>']
|
||||
hasLeadingParen = contactString[match.index-1] in ['(','<']
|
||||
hasTrailingParen = contactString[match.index+email.length] in [')','>']
|
||||
|
||||
if hasLeadingParen and hasTrailingParen
|
||||
nameStart = 0
|
||||
for char in ['>', ')', ',', '\n', '\r']
|
||||
i = str.lastIndexOf(char, match.index)
|
||||
i = contactString.lastIndexOf(char, match.index)
|
||||
nameStart = i+1 if i+1 > nameStart
|
||||
name = str.substr(nameStart, match.index - 1 - nameStart).trim()
|
||||
name = contactString.substr(nameStart, match.index - 1 - nameStart).trim()
|
||||
|
||||
if not name or name.length is 0
|
||||
if (not name or name.length is 0) and not skipNameLookup
|
||||
# Look to see if we can find a name for this email address in the ContactStore.
|
||||
# Otherwise, just populate the name with the email address.
|
||||
existing = @searchContacts(email, {limit:1})[0]
|
||||
|
|
|
@ -12,10 +12,14 @@ progress = require 'request-progress'
|
|||
NamespaceStore = require '../stores/namespace-store'
|
||||
NylasAPI = require '../nylas-api'
|
||||
|
||||
UNTITLED = "Untitled"
|
||||
|
||||
class Download
|
||||
constructor: ({@fileId, @targetPath, @filename, @filesize, @progressCallback}) ->
|
||||
@percent = 0
|
||||
@promise = null
|
||||
if (@filename ? "").trim().length is 0
|
||||
@filename = UNTITLED
|
||||
@
|
||||
|
||||
state: ->
|
||||
|
@ -222,4 +226,10 @@ FileDownloadStore = Reflux.createStore
|
|||
if not fs.existsSync(downloadDir)
|
||||
downloadDir = os.tmpdir()
|
||||
|
||||
path.join(downloadDir, file.filename)
|
||||
path.join(downloadDir, @_filename(file.filename))
|
||||
|
||||
# Sometimes files can have no name.
|
||||
_filename: (filename="") ->
|
||||
if filename.trim().length is 0
|
||||
return UNTITLED
|
||||
else return filename
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
// A box to hold counts of things (like number of items in a tag, or
|
||||
// number of messages in a thread)
|
||||
.item-count-box {
|
||||
float: right;
|
||||
min-width: 26px;
|
||||
text-align: center;
|
||||
display: inline;
|
||||
font-size: @font-size-smaller;
|
||||
padding: @padding-xs-vertical @padding-xs-horizontal @padding-xs-vertical @padding-xs-horizontal;
|
||||
|
|
Loading…
Reference in a new issue