diff --git a/exports/nylas-component-kit.coffee b/exports/nylas-component-kit.coffee index 4b36de1a9..32e66f68c 100644 --- a/exports/nylas-component-kit.coffee +++ b/exports/nylas-component-kit.coffee @@ -4,6 +4,8 @@ GeneratedForm, GeneratedFieldset} = require ('../src/components/generated-form') +{MailLabel, LabelColorizer} = require '../src/components/mail-label' + module.exports = # Models Menu: require '../src/components/menu' @@ -23,7 +25,8 @@ module.exports = InjectedComponent: require '../src/components/injected-component' TokenizingTextField: require '../src/components/tokenizing-text-field' TimeoutTransitionGroup: require '../src/components/timeout-transition-group' - MailLabel: require '../src/components/mail-label' + MailLabel: MailLabel + LabelColorizer: LabelColorizer FormItem: FormItem GeneratedForm: GeneratedForm GeneratedFieldset: GeneratedFieldset diff --git a/internal_packages/category-picker/lib/category-picker.cjsx b/internal_packages/category-picker/lib/category-picker.cjsx index 03086d07e..70c96cefc 100644 --- a/internal_packages/category-picker/lib/category-picker.cjsx +++ b/internal_packages/category-picker/lib/category-picker.cjsx @@ -1,43 +1,43 @@ _ = require 'underscore' React = require 'react' -{Actions, +{Utils, + Thread, + Actions, TaskQueue, CategoryStore, NamespaceStore, + WorkspaceStore, ChangeLabelsTask, ChangeFolderTask, - FocusedContentStore} = require 'nylas-exports' + FocusedContentStore, + FocusedCategoryStore} = require 'nylas-exports' {Menu, Popover, - RetinaImg} = require 'nylas-component-kit' + RetinaImg, + LabelColorizer} = 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: "" + @contextTypes: + sheetDepth: React.PropTypes.number + componentDidMount: => @unsubscribers = [] @unsubscribers.push CategoryStore.listen @_onStoreChanged @unsubscribers.push NamespaceStore.listen @_onStoreChanged @unsubscribers.push FocusedContentStore.listen @_onStoreChanged + @unsubscribers.push FocusedCategoryStore.listen @_onStoreChanged @_commandUnsubscriber = atom.commands.add 'body', - "application:change-category": @_onChangeCategory + "application:change-category": @_onOpenCategoryPopover # If the threads we're picking categories for change, (like when they # get their categories updated), we expect our parents to pass us new @@ -52,9 +52,25 @@ class CategoryPicker extends React.Component @_commandUnsubscriber.dispose() render: => - button = headerComponents = [ @@ -62,39 +78,99 @@ class CategoryPicker extends React.Component tabIndex="1" key="textfield" className="search" + placeholder={placeholder} value={@state.searchValue} onChange={@_onSearchValueChange}/> ] - categoryDatum.id } - itemContent={@_itemContent} - itemChecked={ (categoryDatum) -> categoryDatum.usage > 0 } + itemContent={@_renderItemContent} onSelect={@_onSelectCategory} + defaultSelectedIndex={if @state.searchValue is "" then -1 else 0} /> - _tooltipLabel: -> - return "" unless @_namespace - if @_namespace.usesLabels() - return "Apply Labels" - else if @_namespace.usesFolders() - return "Move to Folder" - - _onChangeCategory: => + _onOpenCategoryPopover: => return unless @_threads().length > 0 + return unless @context.sheetDepth is WorkspaceStore.sheetStack().length - 1 @refs.popover.open() + return - _itemContent: (categoryDatum) => - {categoryDatum.display_name} + _renderItemContent: (categoryDatum) => + if categoryDatum.divider + return + if @_namespace?.usesLabels() + icon = @_renderCheckbox(categoryDatum) + else if @_namespace?.usesFolders() + icon = @_renderFolderIcon(categoryDatum) + else return + +
+ {icon} +
+ {@_renderBoldedSearchResults(categoryDatum)} +
+
+ + _renderCheckbox: (categoryDatum) -> + styles = {} + styles.backgroundColor = categoryDatum.backgroundColor + + if categoryDatum.usage is 0 + checkStatus = + else if categoryDatum.usage < categoryDatum.numThreads + checkStatus = @_onSelectCategory(categoryDatum)}/> + else + checkStatus = @_onSelectCategory(categoryDatum)}/> + +
+ @_onSelectCategory(categoryDatum)}/> + {checkStatus} +
+ + _renderFolderIcon: (categoryDatum) -> + + + _renderBoldedSearchResults: (categoryDatum) -> + name = categoryDatum.display_name + searchTerm = (@state.searchValue ? "").trim() + + return name if searchTerm.length is 0 + + re = Utils.wordSearchRegExp(searchTerm) + parts = name.split(re).map (part) -> + # The wordSearchRegExp looks for a leading non-word character to + # deterine if it's a valid place to search. As such, we need to not + # include that leading character as part of our match. + if re.test(part) + if /\W/.test(part[0]) + return {part[0]}{part[1..-1]} + else + return {part} + else return part + return {parts} _onSelectCategory: (categoryDatum) => return unless @_threads().length > 0 @@ -116,11 +192,12 @@ class CategoryPicker extends React.Component threadIds: @_threadIds() if @props.thread Actions.moveThread(@props.thread, task) - else if @props.selection + else if @props.items Actions.moveThreads(@_threads(), task) else throw new Error("Invalid organizationUnit") + @refs.popover.close() TaskQueue.enqueue(task) _onStoreChanged: => @@ -131,20 +208,33 @@ class CategoryPicker extends React.Component _onPopoverOpened: => @setState @_recalculateState(@props, searchValue: "") + @setState isPopoverOpen: true + + _onPopoverClosed: => + @setState isPopoverOpen: false _recalculateState: (props=@props, {searchValue}={}) => searchValue = searchValue ? @state?.searchValue ? "" - if @_threads(props).length is 0 + numThreads = @_threads(props).length + if numThreads is 0 return {categoryData: [], searchValue} @_namespace = NamespaceStore.current() return unless @_namespace - categories = CategoryStore.getCategories() + categories = [].concat(CategoryStore.getStandardCategories()) + .concat([{divider: true}]) + .concat(CategoryStore.getUserCategories()) + usageCount = @_categoryUsageCount(props, categories) + + allInInbox = @_allInInbox(usageCount, numThreads) + + displayData = {usageCount, numThreads} + categoryData = _.chain(categories) - .filter(@_isUserFacing) + .filter(_.partial(@_isUserFacing, allInInbox)) .filter(_.partial(@_isInSearch, searchValue)) - .map(_.partial(@_extendCategoryWithUsage, usageCount)) + .map(_.partial(@_extendCategoryWithDisplayData, displayData)) .value() return {categoryData, searchValue} @@ -157,34 +247,32 @@ class CategoryPicker extends React.Component return categoryUsageCount _isInSearch: (searchValue, category) -> - searchTerm = searchValue.trim().toLowerCase() - return true if searchTerm.length is 0 + return Utils.wordSearchRegExp(searchValue).test(category.displayName) - catName = category.displayName.trim().toLowerCase() + _isUserFacing: (allInInbox, category) => + hiddenCategories = [] + currentCategoryId = FocusedCategoryStore.categoryId() + if @_namespace?.usesLabels() + hiddenCategories = ["all", "spam", "trash", "drafts", "sent"] + if allInInbox + hiddenCategories.push("inbox") + return false if category.divider + else if @_namespace?.usesFolders() + hiddenCategories = ["drafts", "sent"] - 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 + return (category.name not in hiddenCategories) and (category.id isnt currentCategoryId) - # Always include the start - wordIndices.push(0) + _allInInbox: (usageCount, numThreads) -> + inbox = CategoryStore.getStandardCategory("inbox") + return usageCount[inbox.id] is numThreads - 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) -> + _extendCategoryWithDisplayData: ({usageCount, numThreads}, category) -> + return category if category.divider cat = category.toJSON() usage = usageCount[cat.id] ? 0 + cat.backgroundColor = LabelColorizer.backgroundColorDark(category) cat.usage = usage - cat.totalUsage = categories.length + cat.numThreads = numThreads return cat _threadCategories: (thread) => @@ -195,7 +283,7 @@ class CategoryPicker extends React.Component else throw new Error("Invalid organizationUnit") _threads: (props=@props) => - if props.selection then return (props.selection.items() ? []) + if props.items then return (props.items ? []) else if props.thread then return [props.thread] else return [] diff --git a/internal_packages/category-picker/stylesheets/category-picker.less b/internal_packages/category-picker/stylesheets/category-picker.less index 1d0653412..dc41c6662 100644 --- a/internal_packages/category-picker/stylesheets/category-picker.less +++ b/internal_packages/category-picker/stylesheets/category-picker.less @@ -1,4 +1,72 @@ @import "ui-variables"; .category-picker { + .popover { + } + + .menu { + + background: @background-secondary; + + .header-container { + border-bottom: 0; + } + + .item.selected:hover { + background-color: @accent-primary; + color: @text-color-inverse; + } + + .item:hover { + background-color: transparent; + color: inherit; + .primary, .secondary { + color: inherit; + } + } + + .item.divider { + background-color: #e0e0e0; + margin: 4px 0; + height: 2px; + padding: 0; + } + + } + + .btn.btn-toolbar { + margin-left: 0; + margin-right: 0; + } + + .check-wrap { + width: 14px; + height: 14px; + border-radius: 2px; + display: inline-block; + position: relative; + top: 2px; + } + + img.content-mask { + background-color: @text-color-subtle; + position: relative; + top: -1px; + } + .item.selected img.content-mask { + background-color: @text-color-inverse; + } + + img.check-img { + position: absolute; + } + + .category-item { + font-size: 14px; + } + + .category-display-name { + display: inline-block; + margin-left: 10px; + } } diff --git a/internal_packages/message-list/lib/thread-archive-button.cjsx b/internal_packages/message-list/lib/thread-archive-button.cjsx index 387797ed2..86f2d1fa1 100644 --- a/internal_packages/message-list/lib/thread-archive-button.cjsx +++ b/internal_packages/message-list/lib/thread-archive-button.cjsx @@ -5,6 +5,7 @@ React = require 'react' class ThreadArchiveButton extends React.Component @displayName: "ThreadArchiveButton" + @containerRequired: false render: => - - headerComponents = [ - - ] - - - item.id } - itemContent={@_itemContent} - itemChecked={@_itemChecked} - onSelect={@_onToggleTag} - /> - - - _itemContent: (tag) => - if tag.id is 'divider' - - else - tag.name.charAt(0).toUpperCase() + tag.name.slice(1) - - _itemChecked: (tag) => - return false unless @state.thread - @state.thread.hasCategoryId(tag.id) - - _onShowTags: => - # Always reset search state when the popover is shown - if @state.searchValue.length > 0 - @setState @_getStateForSearch('') - - _onToggleTag: (tag) => - return unless @state.thread - - if @state.thread.hasCategoryId(tag.id) - task = new AddRemoveTagsTask(@state.thread, [], [tag.id]) - else - task = new AddRemoveTagsTask(@state.thread, [tag.id], []) - - @refs.menu.setSelectedItem(null) - - TaskQueue.enqueue(task) - - _onFocusChange: (change) => - if change.impactsCollection('thread') - @_onStoreChange() - - _onStoreChange: => - @setState @_getStateForSearch(@state.searchValue) - - _onSearchValueChange: (event) => - @setState @_getStateForSearch(event.target.value) - - _getStateForSearch: (searchValue = '') => - searchTerm = searchValue.toLowerCase() - thread = FocusedContentStore.focused('thread') - return [] unless thread - - tags = _.filter TagsStore.items(), (tag) -> tag.name.toLowerCase().indexOf(searchTerm) is 0 - - # Some tags are "magic" state and can't be added/removed - tags = _.filter tags, (tag) -> not (tag.id in ['unseen', 'attachment', 'sending', 'drafts', 'sent']) - - # Some tags are readonly - tags = _.filter tags, (tag) -> not tag.readonly - - # Organize tags into currently applied / not applied - active = [] - inactive = [] - for tag in tags - if thread.hasCategoryId(tag.id) - active.push(tag) - else - inactive.push(tag) - - {searchValue, thread, tags: [].concat(active, inactive)} - -module.exports = ThreadTagsButton diff --git a/internal_packages/tooltip/lib/tooltip.cjsx b/internal_packages/tooltip/lib/tooltip.cjsx index f65bc4199..d83134ae6 100644 --- a/internal_packages/tooltip/lib/tooltip.cjsx +++ b/internal_packages/tooltip/lib/tooltip.cjsx @@ -35,6 +35,7 @@ class Tooltip extends React.Component componentWillUnmount: => clearTimeout @_showTimeout clearTimeout @_showDelayTimeout + @_mutationObserver?.disconnect() render: =>
@@ -72,6 +73,11 @@ class Tooltip extends React.Component return target _onTooltipEnter: (target) => + # https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver + @_mutationObserver?.disconnect() + @_mutationObserver = new MutationObserver @_onTooltipLeave + @_mutationObserver.observe(target.parentNode, attributes: true, subtree: true, childList: true) + @_lastTarget = target @_enteredTooltip = true clearTimeout(@_showTimeout) @@ -82,6 +88,7 @@ class Tooltip extends React.Component _onTooltipLeave: => return unless @_enteredTooltip + @_mutationObserver?.disconnect() @_enteredTooltip = false clearTimeout(@_showTimeout) @_hideTooltip() diff --git a/keymaps/base.cson b/keymaps/base.cson index 50d3c1532..60e0cf0c4 100644 --- a/keymaps/base.cson +++ b/keymaps/base.cson @@ -206,13 +206,12 @@ 'tab': 'native!' 'shift-tab': 'native!' - - # For menus 'body .menu, body .menu, body .menu input': - 'up': 'menu:move-up' - 'down': 'menu:move-down' - 'enter': 'menu:enter' + # and by "native!" I actually mean for it to just let React deal with + # it. + 'tab': 'native!' + 'shift-tab': 'native!' # For Popover Component 'body .popover-container, body .popover-container input': diff --git a/src/components/mail-label.cjsx b/src/components/mail-label.cjsx index 0158096c3..fc970d34f 100644 --- a/src/components/mail-label.cjsx +++ b/src/components/mail-label.cjsx @@ -2,19 +2,25 @@ React = require 'react' RetinaImg = require './retina-img' CategoryStore = require '../flux/stores/category-store' +LabelColorizer = + + color: (label) -> "hsl(#{label.hue()}, 50%, 34%)" + + backgroundColor: (label) -> "hsl(#{label.hue()}, 62%, 87%)" + backgroundColorDark: (label) -> "hsl(#{label.hue()}, 62%, 57%)" + + styles: (label) -> + color: LabelColorizer.color(label) + backgroundColor: LabelColorizer.backgroundColor(label) + boxShadow: "inset 0 0 1px hsl(#{label.hue()}, 62%, 47%), inset 0 1px 1px rgba(255,255,255,0.5), 0 0.5px 0 rgba(255,255,255,0.5)" + backgroundImage: 'linear-gradient(rgba(255,255,255, 0.4), rgba(255,255,255,0))' + class MailLabel extends React.Component @propTypes: label: React.PropTypes.object.isRequired onRemove: React.PropTypes.function render: -> - hue = @props.label.hue() - style = - backgroundColor: "hsl(#{hue}, 62%, 87%)" - color: "hsl(#{hue}, 50%, 34%)" - boxShadow: "inset 0 0 1px hsl(#{hue}, 62%, 47%), inset 0 1px 1px rgba(255,255,255,0.5), 0 0.5px 0 rgba(255,255,255,0.5)" - backgroundImage: 'linear-gradient(rgba(255,255,255, 0.4), rgba(255,255,255,0))' - classname = 'mail-label' content = @props.label.displayName @@ -25,14 +31,14 @@ class MailLabel extends React.Component x = -
{content}{x}
+
{content}{x}
_removable: -> isLockedLabel = @props.label.name in CategoryStore.LockedCategoryNames return @props.onRemove and not isLockedLabel -module.exports = MailLabel +module.exports = {MailLabel, LabelColorizer} diff --git a/src/components/menu.cjsx b/src/components/menu.cjsx index b51813f30..a2a01230d 100644 --- a/src/components/menu.cjsx +++ b/src/components/menu.cjsx @@ -126,6 +126,10 @@ class Menu extends React.Component - `onSelect` A {Function} called with the selected item when the user clicks an item in the menu or confirms their selection with the Enter key. + - `defaultSelectedIndex` The index of the item first selected if there + was no other previous index. Defaults to 0. Set to -1 if you want + nothing selected. + ### @propTypes: className: React.PropTypes.string, @@ -139,9 +143,11 @@ class Menu extends React.Component onSelect: React.PropTypes.func.isRequired, + defaultSelectedIndex: React.PropTypes.number + constructor: (@props) -> @state = - selectedIndex: 0 + selectedIndex: @props.defaultSelectedIndex ? 0 # Public: Returns the currently selected item. # @@ -152,16 +158,7 @@ class Menu extends React.Component # null to remove the selection # setSelectedItem: (item) => - @setState - selectedIndex: @props.items.indexOf(item) - - componentDidMount: => - @subscriptions = new CompositeDisposable() - @subscriptions.add atom.commands.add '.menu', { - 'menu:move-up': => @_onShiftSelectedIndex(-1) - 'menu:move-down': => @_onShiftSelectedIndex(1) - 'menu:enter': => @_onEnter() - } + @setState selectedIndex: @props.items.indexOf(item) componentWillReceiveProps: (newProps) => # Attempt to preserve selection across props.items changes by @@ -171,7 +168,7 @@ class Menu extends React.Component selection = @props.items[@state.selectedIndex] newSelectionIndex = 0 else - newSelectionIndex = -1 + newSelectionIndex = newProps.defaultSelectedIndex ? -1 if selection? selectionKey = @props.itemKey(selection) @@ -188,15 +185,14 @@ class Menu extends React.Component if adjustment isnt 0 container.scrollTop += adjustment - componentWillUnmount: => - @subscriptions?.dispose() - render: => hc = @props.headerComponents ? [] if hc.length is 0 then hc = fc = @props.footerComponents ? [] if fc.length is 0 then fc = -
+
{hc}
@@ -206,6 +202,18 @@ class Menu extends React.Component
+ _onKeyDown: (event) => + if event.key is "Enter" + @_onEnter() + else if event.key is "ArrowUp" or (event.key is "Tab" and event.shiftKey) + @_onShiftSelectedIndex(-1) + event.preventDefault() + else if event.key is "ArrowDown" or event.key is "Tab" + @_onShiftSelectedIndex(1) + event.preventDefault() + + return + _contentContainer: => items = @props.items.map(@_itemComponentForItem) ? [] contentClass = classNames @@ -234,18 +242,22 @@ class Menu extends React.Component _onShiftSelectedIndex: (delta) => return if @props.items.length is 0 + index = @state.selectedIndex + delta + + isDivider = true + while isDivider + item = @props.items[index] + break unless item + if @props.itemContent(item).props?.divider + if delta > 0 then index += 1 + else if delta < 0 then index -= 1 + else isDivider = false + index = Math.max(0, Math.min(@props.items.length-1, index)) # Update the selected index - @setState - selectedIndex: index - - # Fire the shift method again to move selection past the divider - # if the new selected item is a divider. - itemContent = @props.itemContent(@props.items[index]) - isDivider = itemContent.props?.divider - @_onShiftSelectedIndex(delta) if isDivider + @setState selectedIndex: index _onEnter: => item = @props.items[@state.selectedIndex] diff --git a/src/components/multiselect-action-bar.cjsx b/src/components/multiselect-action-bar.cjsx index fe6a5b387..c6c263b4d 100644 --- a/src/components/multiselect-action-bar.cjsx +++ b/src/components/multiselect-action-bar.cjsx @@ -2,7 +2,8 @@ React = require "react/addons" classNames = require 'classnames' _ = require 'underscore' -{Actions, +{Utils, + Actions, WorkspaceStore} = require "nylas-exports" InjectedComponentSet = require './injected-component-set' RetinaImg = require './retina-img' @@ -75,10 +76,8 @@ class MultiselectActionBar extends React.Component @unsubscribers.push WorkspaceStore.listen @_onChange shouldComponentUpdate: (nextProps, nextState) => - @props.collection isnt nextProps.collection or - @state.count isnt nextState.count or - @state.view isnt nextState.view or - @state.type isnt nextState.type + not Utils.isEqualReact(nextProps, @props) or + not Utils.isEqualReact(nextState, @state) render: =>
@@ -98,27 +97,30 @@ class MultiselectActionBar extends React.Component _renderActions: => return
unless @state.view + exposedProps={selection: @state.view.selection, items: @state.items} /> _label: => - if @state.count > 1 - "#{@state.count} #{@props.collection}s selected" - else if @state.count is 1 - "#{@state.count} #{@props.collection} selected" + if @state.items.length > 1 + "#{@state.items.length} #{@props.collection}s selected" + else if @state.items.length is 1 + "#{@state.items.length} #{@props.collection} selected" else "" _classSet: => classNames "selection-bar": true - "enabled": @state.count > 0 + "enabled": @state.items.length > 0 _getStateFromStores: (props) => props ?= @props view = props.dataStore.view() + items = view?.selection.items() ? [] - view: view - count: view?.selection.items().length + return { + view: view + items: items + } _onChange: => @setState(@_getStateFromStores()) diff --git a/src/components/popover.cjsx b/src/components/popover.cjsx index f8fa595e8..3bf98938b 100644 --- a/src/components/popover.cjsx +++ b/src/components/popover.cjsx @@ -87,12 +87,12 @@ class Popover extends React.Component @_focusOnOpen = true @setState showing: true - if @props.onOpened? - @props.onOpened() + @props.onOpened?() close: => @setState showing: false + @props.onClosed?() _focusImportantElement: => # Automatically focus the element inside us with the lowest tab index @@ -143,6 +143,14 @@ class Popover extends React.Component 'top': -10 'left':-12 + if @props.direction is "down-align-left" + popoverStyle = _.extend popoverStyle, + 'transform': 'translate(0, 2px)' + 'top': '100%' + 'left': 0 + pointerStyle = _.extend pointerStyle, + 'display': 'none' + popoverComponent =
{@props.children}
diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee index 921cb1102..b7a1c5e8a 100644 --- a/src/flux/models/utils.coffee +++ b/src/flux/models/utils.coffee @@ -11,6 +11,17 @@ Utils = isHash: (object) -> _.isObject(object) and not _.isFunction(object) and not _.isArray(object) + escapeRegExp: (str) -> + str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") + + # Generates a new RegExp that is great for basic search fields. It + # checks if the test string is at the start of words + # + # See regex explanation and test here: + # https://regex101.com/r/zG7aW4/2 + wordSearchRegExp: (str="") -> + new RegExp("((?:^|\\W|$)#{Utils.escapeRegExp(str.trim())})", "ig") + # Takes an optional customizer. The customizer is passed the key and the # new cloned value for that key. The customizer is expected to either # modify the value and return it or simply be the identity function. diff --git a/src/flux/stores/model-view-selection.coffee b/src/flux/stores/model-view-selection.coffee index 4635b997e..de5fd16f3 100644 --- a/src/flux/stores/model-view-selection.coffee +++ b/src/flux/stores/model-view-selection.coffee @@ -14,8 +14,7 @@ class ModelViewSelection ids: -> _.pluck(@_items, 'id') - items: -> - @_items + items: -> _.clone(@_items) top: -> @_items[@_items.length - 1] diff --git a/src/flux/tasks/change-labels-task.coffee b/src/flux/tasks/change-labels-task.coffee index 13ae143f8..0471a93ab 100644 --- a/src/flux/tasks/change-labels-task.coffee +++ b/src/flux/tasks/change-labels-task.coffee @@ -18,7 +18,7 @@ class ChangeLabelsTask extends ChangeCategoryTask constructor: ({@labelsToAdd, @labelsToRemove, @threadIds, @messageIds}={}) -> @threadIds ?= []; @messageIds ?= [] @objectIds = @threadIds.concat(@messageIds) - @_newLabels = [] + @_newLabels = {} super label: -> "Applying labels…" @@ -46,7 +46,7 @@ class ChangeLabelsTask extends ChangeCategoryTask return Promise.resolve(Task.Status.Finished) requestBody: (id) -> - labels: @_newLabels.map (l) -> l.id + labels: @_newLabels[id].map (l) -> l.id createUndoTask: -> labelsToAdd = @labelsToRemove @@ -59,7 +59,7 @@ class ChangeLabelsTask extends ChangeCategoryTask # Called from super-class's `performLocal` localUpdateThread: (thread, categories) -> newLabels = @_newLabelSet(thread, categories) - @_newLabels = newLabels + @_newLabels[thread.id] = newLabels messageQuery = DatabaseStore.findAll(Message, threadId: thread.id) childSavePromise = messageQuery.then (messages) -> @@ -79,7 +79,7 @@ class ChangeLabelsTask extends ChangeCategoryTask # Called from super-class's `performLocal` localUpdateMessage: (message, categories) -> message.labels = @_newLabelSet(message, categories) - @_newLabels = message.labels + @_newLabels[message.id] = message.labels return DatabaseStore.persistModel(message) # Returns a new set of {Label} objects that incoprates the existing, diff --git a/src/sheet-toolbar.cjsx b/src/sheet-toolbar.cjsx index 67c9a8dfd..925bf87a2 100644 --- a/src/sheet-toolbar.cjsx +++ b/src/sheet-toolbar.cjsx @@ -84,6 +84,11 @@ class Toolbar extends React.Component data: React.PropTypes.object depth: React.PropTypes.number + @childContextTypes: + sheetDepth: React.PropTypes.number + getChildContext: => + sheetDepth: @props.depth + constructor: (@props) -> @state = @_getStateFromStores() diff --git a/src/sheet.cjsx b/src/sheet.cjsx index 5e2f4e1f0..c82457fa8 100644 --- a/src/sheet.cjsx +++ b/src/sheet.cjsx @@ -16,6 +16,11 @@ class Sheet extends React.Component depth: React.PropTypes.number.isRequired onColumnSizeChanged: React.PropTypes.func + @childContextTypes: + sheetDepth: React.PropTypes.number + getChildContext: => + sheetDepth: @props.depth + constructor: (@props) -> @state = @_getStateFromStores() diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 86e44ebd4..f06eb4c78 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -121,7 +121,6 @@ class WindowEventHandler onKeydown: (event) -> atom.keymaps.handleKeyboardEvent(event) - event.stopImmediatePropagation() # Important: even though we don't do anything here, we need to catch the # drop event to prevent the browser from navigating the to the "url" of the diff --git a/static/components/menu.less b/static/components/menu.less index 755fdbb45..ceff4a59a 100644 --- a/static/components/menu.less +++ b/static/components/menu.less @@ -1,28 +1,39 @@ @import "ui-variables"; .menu { + flex: 1; + display: flex; + flex-direction: column; + .header-container { + flex-shrink: 0; // Don't squish the header! There may be a search box here background-color: @background-color-secondary; - border-bottom:1px solid @base-border-color; - padding:8px; + border-bottom: 1px solid @base-border-color; + padding: 3px; position: relative; input.search { - border:1px solid darken(@background-color-secondary, 10%); + border: 1px solid darken(@background-color-secondary, 10%); + border-radius: 3px; + padding-left: 0; background-color: white; - border-radius:12px; - padding-left: 10px; + background-repeat: no-repeat; + background-image: url("../static/images/search/searchloupe@2x.png"); + background-size: 15px 15px; + background-position: 7px 4px; + text-indent: 31px; + box-shadow: inset 0 1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(0,0,0,0.05) } } .footer-container { position: relative; - border-top:1px solid @base-border-color; } .content-container { background: @background-color-secondary; width: 100%; margin-top: -1px; + overflow: auto; } .item { @@ -30,7 +41,7 @@ padding-left: @padding-base-horizontal; padding-top: @padding-base-vertical; padding-bottom: @padding-base-vertical; - cursor: pointer; + cursor: default; color: @text-color; width: 100%; overflow: hidden; @@ -74,7 +85,7 @@ background-image:url(./images/menu/checked-selected@2x.png); } - .item.selected, .item:hover { + .item.selected, .item:hover, .item:active { text-decoration: none; background-color: @accent-primary; color: @text-color-inverse; diff --git a/static/components/popover.less b/static/components/popover.less index 8e777d2db..5eb0a2a78 100644 --- a/static/components/popover.less +++ b/static/components/popover.less @@ -6,11 +6,14 @@ } .popover { + display: flex; + flex-direction: column; width: 250px; max-height:400px; background-color: @background-color; border-radius: @border-radius-base; - box-shadow: 0 4px 30px rgba(0,0,0,0.19), inset 0 0 1px rgba(0,0,0,0.5); + overflow: hidden; + box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 4px 7px rgba(0,0,0,0.15), inset 0 0 1px rgba(0,0,0,0.5); .menu { z-index:1; diff --git a/static/images/labels/tagging-checkbox@2x.png b/static/images/labels/tagging-checkbox@2x.png new file mode 100644 index 000000000..00f5c14b6 Binary files /dev/null and b/static/images/labels/tagging-checkbox@2x.png differ diff --git a/static/images/labels/tagging-checkmark@2x.png b/static/images/labels/tagging-checkmark@2x.png new file mode 100644 index 000000000..0132c0b60 Binary files /dev/null and b/static/images/labels/tagging-checkmark@2x.png differ diff --git a/static/images/labels/tagging-conflicted@2x.png b/static/images/labels/tagging-conflicted@2x.png new file mode 100644 index 000000000..d6a97b521 Binary files /dev/null and b/static/images/labels/tagging-conflicted@2x.png differ diff --git a/static/images/search/searchloupe@2x.png b/static/images/search/searchloupe@2x.png new file mode 100644 index 000000000..d9f68ed54 Binary files /dev/null and b/static/images/search/searchloupe@2x.png differ diff --git a/static/images/toolbar/ic-toolbar-movetofolder@2x.png b/static/images/toolbar/ic-toolbar-movetofolder@2x.png new file mode 100644 index 000000000..7a5e54ff9 Binary files /dev/null and b/static/images/toolbar/ic-toolbar-movetofolder@2x.png differ diff --git a/static/images/toolbar/ic-toolbar-tag@2x.png b/static/images/toolbar/ic-toolbar-tag@2x.png new file mode 100644 index 000000000..d84713f47 Binary files /dev/null and b/static/images/toolbar/ic-toolbar-tag@2x.png differ diff --git a/static/images/toolbar/toolbar-star-selected@2x.png b/static/images/toolbar/toolbar-star-selected@2x.png index da0e82e06..ce1e7455e 100644 Binary files a/static/images/toolbar/toolbar-star-selected@2x.png and b/static/images/toolbar/toolbar-star-selected@2x.png differ diff --git a/static/images/toolbar/toolbar-star@2x.png b/static/images/toolbar/toolbar-star@2x.png index 556b14552..761d8d592 100644 Binary files a/static/images/toolbar/toolbar-star@2x.png and b/static/images/toolbar/toolbar-star@2x.png differ diff --git a/static/images/toolbar/toolbar-tags@2x.png b/static/images/toolbar/toolbar-tags@2x.png deleted file mode 100644 index c0c4ae215..000000000 Binary files a/static/images/toolbar/toolbar-tags@2x.png and /dev/null differ