mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-02 05:19:31 +08:00
feat(labels): add a new label/folder picker
Summary: This is the initial diff for the label picker UI. This is all of the functionality and none of the CSS. Test Plan: todo Reviewers: bengotow Reviewed By: bengotow Subscribers: sdw Differential Revision: https://phab.nylas.com/D1761
This commit is contained in:
parent
a9d6795347
commit
b89fea38c0
18 changed files with 336 additions and 50 deletions
205
internal_packages/category-picker/lib/category-picker.cjsx
Normal file
205
internal_packages/category-picker/lib/category-picker.cjsx
Normal file
|
@ -0,0 +1,205 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
|
||||
{Actions,
|
||||
TaskQueue,
|
||||
CategoryStore,
|
||||
NamespaceStore,
|
||||
ChangeLabelsTask,
|
||||
ChangeFolderTask,
|
||||
FocusedContentStore} = require 'nylas-exports'
|
||||
|
||||
{Menu,
|
||||
Popover,
|
||||
RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
# This changes the category on one or more threads.
|
||||
#
|
||||
# See internal_packages/thread-list/lib/thread-buttons.cjsx
|
||||
# See internal_packages/message-list/lib/thread-tags-button.cjsx
|
||||
# See internal_packages/message-list/lib/thread-archive-button.cjsx
|
||||
# See internal_packages/message-list/lib/message-toolbar-items.cjsx
|
||||
|
||||
class CategoryPicker extends React.Component
|
||||
@displayName: "CategoryPicker"
|
||||
@containerRequired: false
|
||||
|
||||
@propTypes:
|
||||
selection: React.PropTypes.object.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = _.extend @_recalculateState(@props), searchValue: ""
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribers = []
|
||||
@unsubscribers.push CategoryStore.listen @_onStoreChanged
|
||||
@unsubscribers.push NamespaceStore.listen @_onStoreChanged
|
||||
@unsubscribers.push FocusedContentStore.listen @_onStoreChanged
|
||||
|
||||
@_commandUnsubscriber = atom.commands.add 'body',
|
||||
"application:change-category": @_onChangeCategory
|
||||
|
||||
# If the threads we're picking categories for change, (like when they
|
||||
# get their categories updated), we expect our parents to pass us new
|
||||
# props. We don't listen to the DatabaseStore ourselves.
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
@setState @_recalculateState(nextProps)
|
||||
|
||||
componentWillUnmount: =>
|
||||
return unless @unsubscribers
|
||||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
@_commandUnsubscriber.dispose()
|
||||
|
||||
render: =>
|
||||
button = <button className="btn btn-toolbar" data-tooltip={@_tooltipLabel()}>
|
||||
<RetinaImg name="toolbar-tags.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
<RetinaImg name="toolbar-chevron.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
|
||||
headerComponents = [
|
||||
<input type="text"
|
||||
tabIndex="1"
|
||||
key="textfield"
|
||||
className="search"
|
||||
value={@state.searchValue}
|
||||
onChange={@_onSearchValueChange}/>
|
||||
]
|
||||
|
||||
<Popover className="tag-picker"
|
||||
ref="popover"
|
||||
onOpened={@_onPopoverOpened}
|
||||
direction="down"
|
||||
buttonComponent={button}>
|
||||
<Menu ref="menu"
|
||||
headerComponents={headerComponents}
|
||||
footerComponents={[]}
|
||||
items={@state.categoryData}
|
||||
itemKey={ (categoryDatum) -> categoryDatum.id }
|
||||
itemContent={@_itemContent}
|
||||
itemChecked={ (categoryDatum) -> categoryDatum.usage > 0 }
|
||||
onSelect={@_onSelectCategory}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
_tooltipLabel: ->
|
||||
return "" unless @_namespace
|
||||
if @_namespace.usesLabels()
|
||||
return "Apply Labels"
|
||||
else if @_namespace.usesFolders()
|
||||
return "Move to Folder"
|
||||
|
||||
_onChangeCategory: =>
|
||||
return unless @_threads().length > 0
|
||||
@refs.popover.open()
|
||||
|
||||
_itemContent: (categoryDatum) =>
|
||||
<span className="category-item">{categoryDatum.display_name}</span>
|
||||
|
||||
_onSelectCategory: (categoryDatum) =>
|
||||
return unless @_threads().length > 0
|
||||
return unless @_namespace
|
||||
@refs.menu.setSelectedItem(null)
|
||||
|
||||
if @_namespace.usesLabels()
|
||||
if categoryDatum.usage > 0
|
||||
task = new ChangeLabelsTask
|
||||
labelsToRemove: [categoryDatum.id]
|
||||
threadIds: @_threadIds()
|
||||
else
|
||||
task = new ChangeLabelsTask
|
||||
labelsToAdd: [categoryDatum.id]
|
||||
threadIds: @_threadIds()
|
||||
else if @_namespace.usesFolders()
|
||||
task = new ChangeFolderTask
|
||||
folderOrId: categoryDatum.id
|
||||
threadIds: @_threadIds()
|
||||
if @props.thread
|
||||
Actions.moveThread(@props.thread, task)
|
||||
else if @props.selection
|
||||
Actions.moveThreads(@_threads(), task)
|
||||
|
||||
else throw new Error("Invalid organizationUnit")
|
||||
|
||||
TaskQueue.enqueue(task)
|
||||
|
||||
_onStoreChanged: =>
|
||||
@setState @_recalculateState(@props)
|
||||
|
||||
_onSearchValueChange: (event) =>
|
||||
@setState @_recalculateState(@props, searchValue: event.target.value)
|
||||
|
||||
_onPopoverOpened: =>
|
||||
@setState @_recalculateState(@props, searchValue: "")
|
||||
|
||||
_recalculateState: (props=@props, {searchValue}={}) =>
|
||||
searchValue = searchValue ? @state?.searchValue ? ""
|
||||
if @_threads(props).length is 0
|
||||
return {categoryData: [], searchValue}
|
||||
@_namespace = NamespaceStore.current()
|
||||
return unless @_namespace
|
||||
|
||||
categories = CategoryStore.getCategories()
|
||||
usageCount = @_categoryUsageCount(props, categories)
|
||||
categoryData = _.chain(categories)
|
||||
.filter(@_isUserFacing)
|
||||
.filter(_.partial(@_isInSearch, searchValue))
|
||||
.map(_.partial(@_extendCategoryWithUsage, usageCount))
|
||||
.value()
|
||||
|
||||
return {categoryData, searchValue}
|
||||
|
||||
_categoryUsageCount: (props, categories) =>
|
||||
categoryUsageCount = {}
|
||||
_.flatten(@_threads(props).map(@_threadCategories)).forEach (category) ->
|
||||
categoryUsageCount[category.id] ?= 0
|
||||
categoryUsageCount[category.id] += 1
|
||||
return categoryUsageCount
|
||||
|
||||
_isInSearch: (searchValue, category) ->
|
||||
searchTerm = searchValue.trim().toLowerCase()
|
||||
return true if searchTerm.length is 0
|
||||
|
||||
catName = category.displayName.trim().toLowerCase()
|
||||
|
||||
wordIndices = []
|
||||
# Where a non-word character is followed by a word character
|
||||
# We don't use \b (word boundary) because we want to split on slashes
|
||||
# and dashes and other non word things
|
||||
re = /\W[\w\d]/g
|
||||
while match = re.exec(catName) then wordIndices.push(match.index)
|
||||
# To shift to the start of each word.
|
||||
wordIndices = wordIndices.map (i) -> i += 1
|
||||
|
||||
# Always include the start
|
||||
wordIndices.push(0)
|
||||
|
||||
return catName.indexOf(searchTerm) in wordIndices
|
||||
|
||||
_isUserFacing: (category) =>
|
||||
hiddenCategories = ["inbox", "all", "archive", "drafts", "sent"]
|
||||
return category.name not in hiddenCategories
|
||||
|
||||
_extendCategoryWithUsage: (usageCount, category, i, categories) ->
|
||||
cat = category.toJSON()
|
||||
usage = usageCount[cat.id] ? 0
|
||||
cat.usage = usage
|
||||
cat.totalUsage = categories.length
|
||||
return cat
|
||||
|
||||
_threadCategories: (thread) =>
|
||||
if @_namespace.usesLabels()
|
||||
return (thread.labels ? [])
|
||||
else if @_namespace.usesFolders()
|
||||
return (thread.folders ? [])
|
||||
else throw new Error("Invalid organizationUnit")
|
||||
|
||||
_threads: (props=@props) =>
|
||||
if props.selection then return (props.selection.items() ? [])
|
||||
else if props.thread then return [props.thread]
|
||||
else return []
|
||||
|
||||
_threadIds: =>
|
||||
@_threads().map (thread) -> thread.id
|
||||
|
||||
module.exports = CategoryPicker
|
12
internal_packages/category-picker/lib/main.cjsx
Normal file
12
internal_packages/category-picker/lib/main.cjsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
CategoryPicker = require "./category-picker"
|
||||
|
||||
{ComponentRegistry,
|
||||
WorkspaceStore} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
ComponentRegistry.register CategoryPicker,
|
||||
roles: ['thread:BulkAction', 'message:Toolbar']
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister(CategoryPicker)
|
11
internal_packages/category-picker/package.json
Executable file
11
internal_packages/category-picker/package.json
Executable file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "category-picker",
|
||||
"version": "0.1.0",
|
||||
"main": "./lib/main",
|
||||
"description": "Label & Folder Picker",
|
||||
"license": "Proprietary",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"atom": "*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
@import "ui-variables";
|
||||
|
||||
.category-picker {
|
||||
}
|
|
@ -110,11 +110,20 @@ useDraft = (draftAttributes={}) ->
|
|||
@draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
|
||||
draft = @draft
|
||||
proxy = draftStoreProxyStub(DRAFT_LOCAL_ID, @draft)
|
||||
spyOn(DraftStore, "sessionForLocalId").andCallFake -> new Promise (resolve, reject) -> resolve(proxy)
|
||||
|
||||
|
||||
spyOn(ComposerView.prototype, "componentWillMount").andCallFake ->
|
||||
@_prepareForDraft(DRAFT_LOCAL_ID)
|
||||
@_setupSession(proxy)
|
||||
|
||||
# Normally when sessionForLocalId resolves, it will call `_setupSession`
|
||||
# and pass the new session proxy. However, in our faked
|
||||
# `componentWillMount`, we manually call sessionForLocalId to make this
|
||||
# part of the test synchronous. We need to make the `then` block of the
|
||||
# sessionForLocalId do nothing so `_setupSession` is not called twice!
|
||||
spyOn(DraftStore, "sessionForLocalId").andCallFake ->
|
||||
then: ->
|
||||
|
||||
useFullDraft = ->
|
||||
useDraft.call @,
|
||||
from: [u1]
|
||||
|
|
|
@ -4,6 +4,9 @@ MessageToolbarItems = require "./message-toolbar-items"
|
|||
WorkspaceStore} = require 'nylas-exports'
|
||||
SidebarThreadParticipants = require "./sidebar-thread-participants"
|
||||
|
||||
ThreadStarButton = require './thread-star-button'
|
||||
ThreadArchiveButton = require './thread-archive-button'
|
||||
|
||||
module.exports =
|
||||
item: null # The DOM item the main React component renders into
|
||||
|
||||
|
@ -18,8 +21,17 @@ module.exports =
|
|||
ComponentRegistry.register SidebarThreadParticipants,
|
||||
location: WorkspaceStore.Location.MessageListSidebar
|
||||
|
||||
ComponentRegistry.register ThreadStarButton,
|
||||
role: 'message:Toolbar'
|
||||
|
||||
ComponentRegistry.register ThreadArchiveButton,
|
||||
role: 'message:Toolbar'
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister MessageList
|
||||
ComponentRegistry.unregister ThreadStarButton
|
||||
ComponentRegistry.unregister ThreadArchiveButton
|
||||
ComponentRegistry.unregister MessageToolbarItems
|
||||
ComponentRegistry.unregister SidebarThreadParticipants
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{Actions, Utils, FocusedContentStore, WorkspaceStore} = require 'nylas-exports'
|
||||
{RetinaImg, Popover, Menu} = require 'nylas-component-kit'
|
||||
|
||||
ThreadArchiveButton = require './thread-archive-button'
|
||||
ThreadStarButton = require './thread-star-button'
|
||||
{Actions,
|
||||
WorkspaceStore,
|
||||
FocusedContentStore} = require 'nylas-exports'
|
||||
|
||||
{Menu,
|
||||
Popover,
|
||||
RetinaImg,
|
||||
InjectedComponentSet} = require 'nylas-component-kit'
|
||||
|
||||
class MessageToolbarItems extends React.Component
|
||||
@displayName: "MessageToolbarItems"
|
||||
|
@ -20,8 +24,8 @@ class MessageToolbarItems extends React.Component
|
|||
"hidden": !@state.thread
|
||||
|
||||
<div className={classes}>
|
||||
<ThreadArchiveButton />
|
||||
<ThreadStarButton ref="starButton" thread={@state.thread} />
|
||||
<InjectedComponentSet matching={role: "message:Toolbar"}
|
||||
exposedProps={thread: @state.thread}/>
|
||||
</div>
|
||||
|
||||
componentDidMount: =>
|
||||
|
|
|
@ -3,8 +3,8 @@ React = require 'react'
|
|||
{Actions, DOMUtils} = require 'nylas-exports'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class ArchiveButton extends React.Component
|
||||
@displayName: "ArchiveButton"
|
||||
class ThreadArchiveButton extends React.Component
|
||||
@displayName: "ThreadArchiveButton"
|
||||
|
||||
render: =>
|
||||
<button className="btn btn-toolbar btn-archive"
|
||||
|
@ -19,4 +19,4 @@ class ArchiveButton extends React.Component
|
|||
e.stopPropagation()
|
||||
|
||||
|
||||
module.exports = ArchiveButton
|
||||
module.exports = ThreadArchiveButton
|
||||
|
|
|
@ -10,7 +10,7 @@ class StarButton extends React.Component
|
|||
|
||||
render: =>
|
||||
selected = @props.thread? and @props.thread.starred
|
||||
<button className="btn btn-toolbar"
|
||||
<button className="btn btn-toolbar btn-star"
|
||||
data-tooltip="Star"
|
||||
onClick={@_onStarToggle}>
|
||||
<RetinaImg name="toolbar-star.png" mode={RetinaImg.Mode.ContentIsMask} selected={selected} />
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
React = require "react/addons"
|
||||
ReactTestUtils = React.addons.TestUtils
|
||||
TestUtils = React.addons.TestUtils
|
||||
{Thread, FocusedContentStore, Actions} = require "nylas-exports"
|
||||
|
||||
|
||||
MessageToolbarItems = require '../lib/message-toolbar-items'
|
||||
StarButton = require '../lib/thread-star-button'
|
||||
|
||||
test_thread = (new Thread).fromJSON({
|
||||
"id" : "thread_12345"
|
||||
|
@ -19,25 +19,19 @@ test_thread_starred = (new Thread).fromJSON({
|
|||
|
||||
describe "MessageToolbarItem starring", ->
|
||||
it "stars a thread if the star button is clicked and thread is unstarred", ->
|
||||
spyOn(FocusedContentStore, "focused").andCallFake ->
|
||||
test_thread
|
||||
spyOn(Actions, 'queueTask')
|
||||
messageToolbarItems = TestUtils.renderIntoDocument(<MessageToolbarItems />)
|
||||
starButton = TestUtils.renderIntoDocument(<StarButton thread={test_thread}/>)
|
||||
|
||||
starButton = React.findDOMNode(messageToolbarItems.refs.starButton)
|
||||
TestUtils.Simulate.click starButton
|
||||
TestUtils.Simulate.click React.findDOMNode(starButton)
|
||||
|
||||
expect(Actions.queueTask.mostRecentCall.args[0].objects).toEqual([test_thread])
|
||||
expect(Actions.queueTask.mostRecentCall.args[0].newValues).toEqual(starred: true)
|
||||
|
||||
it "unstars a thread if the star button is clicked and thread is starred", ->
|
||||
spyOn(FocusedContentStore, "focused").andCallFake ->
|
||||
test_thread_starred
|
||||
spyOn(Actions, 'queueTask')
|
||||
messageToolbarItems = TestUtils.renderIntoDocument(<MessageToolbarItems />)
|
||||
starButton = TestUtils.renderIntoDocument(<StarButton thread={test_thread_starred}/>)
|
||||
|
||||
starButton = React.findDOMNode(messageToolbarItems.refs.starButton)
|
||||
TestUtils.Simulate.click starButton
|
||||
TestUtils.Simulate.click React.findDOMNode(starButton)
|
||||
|
||||
expect(Actions.queueTask.mostRecentCall.args[0].objects).toEqual([test_thread_starred])
|
||||
expect(Actions.queueTask.mostRecentCall.args[0].newValues).toEqual(starred: false)
|
||||
|
|
|
@ -25,8 +25,12 @@ class ThreadListStore extends NylasStore
|
|||
|
||||
@listenTo Actions.archiveAndPrevious, @_onArchiveAndPrev
|
||||
@listenTo Actions.archiveAndNext, @_onArchiveAndNext
|
||||
|
||||
@listenTo Actions.archiveSelection, @_onArchiveSelection
|
||||
@listenTo Actions.moveThreads, @_onMoveThreads
|
||||
|
||||
@listenTo Actions.archive, @_onArchive
|
||||
@listenTo Actions.moveThread, @_onMoveThread
|
||||
|
||||
@listenTo Actions.toggleStarSelection, @_onToggleStarSelection
|
||||
@listenTo Actions.toggleStarFocused, @_onToggleStarFocused
|
||||
|
@ -143,13 +147,26 @@ class ThreadListStore extends NylasStore
|
|||
_onArchive: ->
|
||||
@_archiveAndShiftBy('auto')
|
||||
|
||||
_onArchiveSelection: ->
|
||||
selectedThreads = @_view.selection.items()
|
||||
selectedThreadIds = selectedThreads.map (thread) -> thread.id
|
||||
_onArchiveAndPrev: ->
|
||||
@_archiveAndShiftBy(-1)
|
||||
|
||||
_onArchiveAndNext: ->
|
||||
@_archiveAndShiftBy(1)
|
||||
|
||||
_archiveAndShiftBy: (offset) ->
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
return unless focused
|
||||
task = ArchiveThreadHelper.getArchiveTask([focused])
|
||||
@_moveAndShiftBy(offset, task)
|
||||
|
||||
_onMoveThread: (thread, task) ->
|
||||
@_moveAndShiftBy('auto', task)
|
||||
|
||||
_onMoveThreads: (threads, task) ->
|
||||
selectedThreadIds = threads.map (thread) -> thread.id
|
||||
focusedId = FocusedContentStore.focusedId('thread')
|
||||
keyboardId = FocusedContentStore.keyboardCursorId('thread')
|
||||
|
||||
task = ArchiveThreadHelper.getArchiveTask(selectedThreads)
|
||||
task.waitForPerformLocal().then =>
|
||||
if focusedId in selectedThreadIds
|
||||
Actions.setFocus(collection: 'thread', item: null)
|
||||
|
@ -159,13 +176,12 @@ class ThreadListStore extends NylasStore
|
|||
Actions.queueTask(task)
|
||||
@_view.selection.clear()
|
||||
|
||||
_onArchiveAndPrev: ->
|
||||
@_archiveAndShiftBy(-1)
|
||||
_onArchiveSelection: ->
|
||||
selectedThreads = @_view.selection.items()
|
||||
task = ArchiveThreadHelper.getArchiveTask(selectedThreads)
|
||||
@_onMoveThreads(selectedThreads, task)
|
||||
|
||||
_onArchiveAndNext: ->
|
||||
@_archiveAndShiftBy(1)
|
||||
|
||||
_archiveAndShiftBy: (offset) ->
|
||||
_moveAndShiftBy: (offset, task) ->
|
||||
layoutMode = WorkspaceStore.layoutMode()
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
explicitOffset = if offset is "auto" then false else true
|
||||
|
@ -196,7 +212,6 @@ class ThreadListStore extends NylasStore
|
|||
nextFocus = null
|
||||
|
||||
# Archive the current thread
|
||||
task = ArchiveThreadHelper.getArchiveTask([focused])
|
||||
task.waitForPerformLocal().then ->
|
||||
Actions.setFocus(collection: 'thread', item: nextFocus)
|
||||
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
|
||||
|
|
|
@ -51,9 +51,12 @@ class Tooltip extends React.Component
|
|||
# This are public methods so they can be bound to the window event
|
||||
# listeners.
|
||||
onMouseOver: (e) =>
|
||||
target = @_elementWithTooltip(e.target)
|
||||
if target and DOMUtils.nodeIsVisible(target) then @_onTooltipEnter(target)
|
||||
else if @state.display then @_hideTooltip()
|
||||
elWithTooltip = @_elementWithTooltip(e.target)
|
||||
if elWithTooltip and DOMUtils.nodeIsVisible(elWithTooltip)
|
||||
if elWithTooltip isnt @_lastTarget
|
||||
@_onTooltipEnter(elWithTooltip)
|
||||
else
|
||||
@_hideTooltip() if @state.display
|
||||
|
||||
onMouseOut: (e) =>
|
||||
if @_elementWithTooltip(e.fromElement) and not @_elementWithTooltip(e.toElement)
|
||||
|
@ -69,6 +72,7 @@ class Tooltip extends React.Component
|
|||
return target
|
||||
|
||||
_onTooltipEnter: (target) =>
|
||||
@_lastTarget = target
|
||||
@_enteredTooltip = true
|
||||
clearTimeout(@_showTimeout)
|
||||
clearTimeout(@_showDelayTimeout)
|
||||
|
@ -91,6 +95,8 @@ class Tooltip extends React.Component
|
|||
_showTooltip: (target) =>
|
||||
return unless DOMUtils.nodeIsVisible(target)
|
||||
content = target.dataset.tooltip
|
||||
return if (content ? "").trim().toLowerCase().length is 0
|
||||
|
||||
guessedWidth = @_guessWidth(content)
|
||||
dim = target.getBoundingClientRect()
|
||||
left = dim.left + dim.width / 2
|
||||
|
@ -146,6 +152,7 @@ class Tooltip extends React.Component
|
|||
document.getElementsByTagName('body')[0].getBoundingClientRect().height
|
||||
|
||||
_hideTooltip: =>
|
||||
@_lastTarget = null
|
||||
@setState
|
||||
top: 0
|
||||
left: 0
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
'R' : 'application:reply-all' # Nylas Mail
|
||||
'a' : 'application:reply-all' # Gmail
|
||||
'f' : 'application:forward' # Gmail
|
||||
'l' : 'application:change-category' # Gmail
|
||||
|
||||
'escape': 'application:pop-sheet'
|
||||
'u' : 'application:pop-sheet' # Gmail
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
'ctrl-r': 'application:reply' # Outlook
|
||||
'ctrl-R': 'application:reply-all' # Outlook
|
||||
'ctrl-F': 'application:forward' # Outlook
|
||||
'ctrl-shift-v': 'application:change-category' # Outlook
|
||||
|
||||
# Linux application keys
|
||||
'ctrl-q': 'application:quit'
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
'ctrl-r': 'application:reply' # Outlook
|
||||
'ctrl-R': 'application:reply-all' # Outlook
|
||||
'ctrl-F': 'application:forward' # Outlook
|
||||
'ctrl-shift-v': 'application:change-category' # Outlook
|
||||
|
||||
# Windows application keys
|
||||
'ctrl-q': 'application:quit'
|
||||
|
|
|
@ -78,7 +78,13 @@ class Popover extends React.Component
|
|||
componentWillUnmount: =>
|
||||
@subscriptions?.dispose()
|
||||
|
||||
componentDidUpdate: ->
|
||||
if @_focusOnOpen
|
||||
@_focusImportantElement()
|
||||
@_focusOnOpen = false
|
||||
|
||||
open: =>
|
||||
@_focusOnOpen = true
|
||||
@setState
|
||||
showing: true
|
||||
if @props.onOpened?
|
||||
|
@ -88,6 +94,19 @@ class Popover extends React.Component
|
|||
@setState
|
||||
showing: false
|
||||
|
||||
_focusImportantElement: =>
|
||||
# Automatically focus the element inside us with the lowest tab index
|
||||
node = React.findDOMNode(@refs.popover)
|
||||
|
||||
# _.sortBy ranks in ascending numerical order.
|
||||
matches = _.sortBy node.querySelectorAll("[tabIndex], input"), (node) ->
|
||||
if node.tabIndex > 0
|
||||
return node.tabIndex
|
||||
else if node.nodeName is "INPUT"
|
||||
return 1000000
|
||||
else return 1000001
|
||||
matches[0]?.focus()
|
||||
|
||||
render: =>
|
||||
wrappedButtonComponent = []
|
||||
if @props.buttonComponent
|
||||
|
@ -137,18 +156,6 @@ class Popover extends React.Component
|
|||
_onClick: =>
|
||||
if not @state.showing
|
||||
@open()
|
||||
setTimeout =>
|
||||
# Automatically focus the element inside us with the lowest tab index
|
||||
node = React.findDOMNode(@refs.popover)
|
||||
|
||||
# _.sortBy ranks in ascending numerical order.
|
||||
matches = _.sortBy node.querySelectorAll("[tabIndex], input"), (node) ->
|
||||
if node.tabIndex > 0
|
||||
return node.tabIndex
|
||||
else if node.nodeName is "INPUT"
|
||||
return 1000000
|
||||
else return 1000001
|
||||
matches[0]?.focus()
|
||||
else
|
||||
@close()
|
||||
|
||||
|
|
|
@ -322,6 +322,9 @@ class Actions
|
|||
@toggleStarFocused: ActionScopeWindow
|
||||
@deleteSelection: ActionScopeWindow
|
||||
|
||||
@moveThread: ActionScopeWindow
|
||||
@moveThreads: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Updates the search query in the app's main search bar with the provided query text.
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class ChangeFolderTask extends ChangeCategoryTask
|
|||
folder: Promise.resolve(@folderOrId)
|
||||
else
|
||||
return Promise.props
|
||||
folder: DatabaseStore.find(Folder, @folderOrId.id)
|
||||
folder: DatabaseStore.find(Folder, @folderOrId)
|
||||
|
||||
# Called from super-class's `performRemote`
|
||||
rollbackLocal: ->
|
||||
|
|
Loading…
Reference in a new issue