diff --git a/internal_packages/category-picker/lib/category-picker-popover.jsx b/internal_packages/category-picker/lib/category-picker-popover.jsx index c95902a81..4de340f1e 100644 --- a/internal_packages/category-picker/lib/category-picker-popover.jsx +++ b/internal_packages/category-picker/lib/category-picker-popover.jsx @@ -206,6 +206,10 @@ export default class CategoryPickerPopover extends Component { }) Actions.queueTask(applyTask) } + if (account.usesFolders()) { + // In case we are drilled down into a message + Actions.popSheet() + } Actions.closePopover() }; diff --git a/internal_packages/category-picker/lib/category-picker.cjsx b/internal_packages/category-picker/lib/category-picker.cjsx index b91346bc8..d15807e42 100644 --- a/internal_packages/category-picker/lib/category-picker.cjsx +++ b/internal_packages/category-picker/lib/category-picker.cjsx @@ -18,38 +18,30 @@ class CategoryPicker extends React.Component @containerRequired: false @propTypes: - thread: React.PropTypes.object items: React.PropTypes.array @contextTypes: sheetDepth: React.PropTypes.number constructor: (@props) -> - @_threads = @_getThreads(@props) - @_account = AccountStore.accountForItems(@_threads) + @_account = AccountStore.accountForItems(@props.items) # 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) -> - @_threads = @_getThreads(nextProps) - @_account = AccountStore.accountForItems(@_threads) - - _getThreads: (props = @props) => - if props.items then return (props.items ? []) - else if props.thread then return [props.thread] - else return [] + @_account = AccountStore.accountForItems(nextProps.items) _keymapHandlers: -> "application:change-category": @_onOpenCategoryPopover _onOpenCategoryPopover: => - return unless @_threads.length > 0 + return unless @props.items.length > 0 return unless @context.sheetDepth is WorkspaceStore.sheetStack().length - 1 buttonRect = React.findDOMNode(@refs.button).getBoundingClientRect() Actions.openPopover( , {originRect: buttonRect, direction: 'down'} ) diff --git a/internal_packages/category-picker/lib/main.cjsx b/internal_packages/category-picker/lib/main.cjsx index ea4a37b9d..bf7f18670 100644 --- a/internal_packages/category-picker/lib/main.cjsx +++ b/internal_packages/category-picker/lib/main.cjsx @@ -6,7 +6,7 @@ CategoryPicker = require "./category-picker" module.exports = activate: (@state={}) -> ComponentRegistry.register CategoryPicker, - roles: ['thread:BulkAction', 'message:Toolbar'] + role: 'ThreadActionsToolbarButton' deactivate: -> ComponentRegistry.unregister(CategoryPicker) diff --git a/internal_packages/thread-list/lib/draft-list-columns.cjsx b/internal_packages/draft-list/lib/draft-list-columns.cjsx similarity index 91% rename from internal_packages/thread-list/lib/draft-list-columns.cjsx rename to internal_packages/draft-list/lib/draft-list-columns.cjsx index 6a859d831..3aa31e3c6 100644 --- a/internal_packages/thread-list/lib/draft-list-columns.cjsx +++ b/internal_packages/draft-list/lib/draft-list-columns.cjsx @@ -1,9 +1,7 @@ _ = require 'underscore' React = require 'react' -classNames = require 'classnames' {Actions} = require 'nylas-exports' {InjectedComponentSet, ListTabular} = require 'nylas-component-kit' -{subject} = require './formatting-utils' snippet = (html) => @@ -16,6 +14,12 @@ snippet = (html) => catch return "" +subject = (subj) -> + if (subj ? "").trim().length is 0 + return (No Subject) + else + return subj + ParticipantsColumn = new ListTabular.Column name: "Participants" width: 200 diff --git a/internal_packages/thread-list/lib/draft-list-send-status.jsx b/internal_packages/draft-list/lib/draft-list-send-status.jsx similarity index 77% rename from internal_packages/thread-list/lib/draft-list-send-status.jsx rename to internal_packages/draft-list/lib/draft-list-send-status.jsx index c5603b252..4742ca8e1 100644 --- a/internal_packages/thread-list/lib/draft-list-send-status.jsx +++ b/internal_packages/draft-list/lib/draft-list-send-status.jsx @@ -1,6 +1,6 @@ import React, {Component, PropTypes} from 'react' +import {Utils} from 'nylas-exports' import {Flexbox} from 'nylas-component-kit' -import {timestamp} from './formatting-utils' import SendingProgressBar from './sending-progress-bar' export default class DraftListSendStatus extends Component { @@ -16,7 +16,7 @@ export default class DraftListSendStatus extends Component { const {draft} = this.props if (draft.uploadTaskId) { return ( - + ) } - return {timestamp(draft.date)} + return {Utils.shortTimeString(draft.date)} } } diff --git a/internal_packages/thread-list/lib/draft-list-store.coffee b/internal_packages/draft-list/lib/draft-list-store.coffee similarity index 96% rename from internal_packages/thread-list/lib/draft-list-store.coffee rename to internal_packages/draft-list/lib/draft-list-store.coffee index 9b6e36480..6046aab05 100644 --- a/internal_packages/thread-list/lib/draft-list-store.coffee +++ b/internal_packages/draft-list/lib/draft-list-store.coffee @@ -18,6 +18,9 @@ class DraftListStore extends NylasStore dataSource: => @_dataSource + selectionObservable: => + return Rx.Observable.fromListSelection(@) + # Inbound Events _onPerspectiveChanged: => diff --git a/internal_packages/draft-list/lib/draft-list-toolbar.jsx b/internal_packages/draft-list/lib/draft-list-toolbar.jsx new file mode 100644 index 000000000..bf359b687 --- /dev/null +++ b/internal_packages/draft-list/lib/draft-list-toolbar.jsx @@ -0,0 +1,50 @@ +import React, {Component, PropTypes} from "react" +import DraftListStore from './draft-list-store' +import {ListensToObservable, MultiselectToolbar, InjectedComponentSet} from 'nylas-component-kit' + + +function getObservable() { + return DraftListStore.selectionObservable() +} + +function getStateFromObservable(items) { + if (!items) { + return {items: []} + } + return {items} +} + +class DraftListToolbar extends Component { + static displayName = 'DraftListToolbar'; + + static propTypes = { + items: PropTypes.array, + }; + + onClearSelection = () => { + DraftListStore.dataSource().selection.clear() + }; + + render() { + const {selection} = DraftListStore.dataSource() + const {items} = this.props + + // Keep all of the exposed props from deprecated regions that now map to this one + const toolbarElement = ( + + ) + + return ( + + ) + } +} + +export default ListensToObservable(DraftListToolbar, {getObservable, getStateFromObservable}) diff --git a/internal_packages/thread-list/lib/draft-list.cjsx b/internal_packages/draft-list/lib/draft-list.cjsx similarity index 91% rename from internal_packages/thread-list/lib/draft-list.cjsx rename to internal_packages/draft-list/lib/draft-list.cjsx index e3ef8cd27..2facca422 100644 --- a/internal_packages/thread-list/lib/draft-list.cjsx +++ b/internal_packages/draft-list/lib/draft-list.cjsx @@ -2,11 +2,11 @@ _ = require 'underscore' React = require 'react' {Actions} = require 'nylas-exports' {FluxContainer, + FocusContainer, + EmptyListState, MultiselectList} = require 'nylas-component-kit' DraftListStore = require './draft-list-store' DraftListColumns = require './draft-list-columns' -FocusContainer = require './focus-container' -EmptyState = require './empty-state' class DraftList extends React.Component @displayName: 'DraftList' @@ -20,7 +20,7 @@ class DraftList extends React.Component + activate: -> # Register Message List Actions we provide globally ComponentRegistry.register MessageList, location: WorkspaceStore.Location.MessageList - ComponentRegistry.register MessageToolbarItems, - location: WorkspaceStore.Location.MessageList.Toolbar - ComponentRegistry.register SidebarParticipantPicker, location: WorkspaceStore.Location.MessageListSidebar + ComponentRegistry.register SidebarPluginContainer, location: WorkspaceStore.Location.MessageListSidebar - ComponentRegistry.register ThreadStarButton, - role: 'message:Toolbar' - - ComponentRegistry.register ThreadArchiveButton, - role: 'message:Toolbar' - - ComponentRegistry.register ThreadTrashButton, - role: 'message:Toolbar' - - ComponentRegistry.register ThreadToggleUnreadButton, - role: 'message:Toolbar' - ComponentRegistry.register MessageListHiddenMessagesToggle, role: 'MessageListHeaders' @@ -50,13 +29,6 @@ module.exports = deactivate: -> ComponentRegistry.unregister MessageList - ComponentRegistry.unregister ThreadStarButton - ComponentRegistry.unregister ThreadArchiveButton - ComponentRegistry.unregister ThreadTrashButton - ComponentRegistry.unregister ThreadToggleUnreadButton - ComponentRegistry.unregister MessageToolbarItems ComponentRegistry.unregister SidebarPluginContainer ComponentRegistry.unregister SidebarParticipantPicker ExtensionRegistry.MessageView.unregister TrackingPixelsExtension - - serialize: -> @state diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx index 6f4677744..c127d4863 100755 --- a/internal_packages/message-list/lib/message-list.cjsx +++ b/internal_packages/message-list/lib/message-list.cjsx @@ -187,7 +187,7 @@ class MessageList extends React.Component render: => if not @state.currentThread - return
+ return wrapClass = classNames "messages-wrap": true diff --git a/internal_packages/message-list/lib/message-toolbar-items.cjsx b/internal_packages/message-list/lib/message-toolbar-items.cjsx deleted file mode 100644 index a9343b85a..000000000 --- a/internal_packages/message-list/lib/message-toolbar-items.cjsx +++ /dev/null @@ -1,46 +0,0 @@ -_ = require 'underscore' -React = require 'react' -classNames = require 'classnames' - -{Actions, - WorkspaceStore, - FocusedContentStore} = require 'nylas-exports' - -{Menu, - RetinaImg, - TimeoutTransitionGroup, - InjectedComponentSet} = require 'nylas-component-kit' - -class MessageToolbarItems extends React.Component - @displayName: "MessageToolbarItems" - - constructor: (@props) -> - @state = - thread: FocusedContentStore.focused('thread') - - render: => - - {@_renderContents()} - - - _renderContents: => - return false unless @state.thread - - - componentDidMount: => - @_unsubscribers = [] - @_unsubscribers.push FocusedContentStore.listen @_onChange - - componentWillUnmount: => - return unless @_unsubscribers - unsubscribe() for unsubscribe in @_unsubscribers - - _onChange: => - @setState - thread: FocusedContentStore.focused('thread') - -module.exports = MessageToolbarItems diff --git a/internal_packages/message-list/spec/message-toolbar-items-spec.cjsx b/internal_packages/message-list/spec/message-toolbar-items-spec.cjsx deleted file mode 100644 index 748369dea..000000000 --- a/internal_packages/message-list/spec/message-toolbar-items-spec.cjsx +++ /dev/null @@ -1,65 +0,0 @@ -React = require "react/addons" -ReactTestUtils = React.addons.TestUtils -TestUtils = React.addons.TestUtils -{Thread, FocusedContentStore, Actions, ChangeUnreadTask} = require "nylas-exports" - -StarButton = require '../lib/thread-star-button' -ThreadToggleUnreadButton = require '../lib/thread-toggle-unread-button' - -test_thread = (new Thread).fromJSON({ - "id" : "thread_12345" - "account_id": TEST_ACCOUNT_ID - "subject" : "Subject 12345" - "starred": false -}) - -test_thread_starred = (new Thread).fromJSON({ - "id" : "thread_starred_12345" - "account_id": TEST_ACCOUNT_ID - "subject" : "Subject 12345" - "starred": true -}) - -describe "MessageToolbarItem starring", -> - it "stars a thread if the star button is clicked and thread is unstarred", -> - spyOn(Actions, 'queueTask') - starButton = TestUtils.renderIntoDocument() - - TestUtils.Simulate.click React.findDOMNode(starButton) - - expect(Actions.queueTask.mostRecentCall.args[0].threads).toEqual([test_thread]) - expect(Actions.queueTask.mostRecentCall.args[0].starred).toEqual(true) - - it "unstars a thread if the star button is clicked and thread is starred", -> - spyOn(Actions, 'queueTask') - starButton = TestUtils.renderIntoDocument() - - TestUtils.Simulate.click React.findDOMNode(starButton) - - expect(Actions.queueTask.mostRecentCall.args[0].threads).toEqual([test_thread_starred]) - expect(Actions.queueTask.mostRecentCall.args[0].starred).toEqual(false) - -describe "MessageToolbarItem marking as unread", -> - thread = null - markUnreadBtn = null - - beforeEach -> - thread = new Thread(id: "thread-id-lol-123", accountId: TEST_ACCOUNT_ID) - markUnreadBtn = ReactTestUtils.renderIntoDocument( - - ) - - it "queues a task to change unread status to true", -> - spyOn Actions, "queueTask" - ReactTestUtils.Simulate.click React.findDOMNode(markUnreadBtn).childNodes[0] - - changeUnreadTask = Actions.queueTask.calls[0].args[0] - expect(changeUnreadTask instanceof ChangeUnreadTask).toBe true - expect(changeUnreadTask.unread).toBe true - expect(changeUnreadTask.threads[0].id).toBe thread.id - - it "returns to the thread list", -> - spyOn Actions, "popSheet" - ReactTestUtils.Simulate.click React.findDOMNode(markUnreadBtn).childNodes[0] - - expect(Actions.popSheet).toHaveBeenCalled() diff --git a/internal_packages/message-view-on-github/lib/main.jsx b/internal_packages/message-view-on-github/lib/main.jsx index 7f22146e2..3edcf69bf 100644 --- a/internal_packages/message-view-on-github/lib/main.jsx +++ b/internal_packages/message-view-on-github/lib/main.jsx @@ -16,10 +16,10 @@ See more details about how this works in the {ComponentRegistry} documentation. In this case the `ViewOnGithubButton` React Component will get rendered -whenever the `"message:Toolbar"` region gets rendered. +whenever the `"MessageList:ThreadActionsToolbarButton"` region gets rendered. Since the `ViewOnGithubButton` doesn't know who owns the -`"message:Toolbar"` region, or even when or where it will be rendered, it +`"MessageList:ThreadActionsToolbarButton"` region, or even when or where it will be rendered, it has to load its internal `state` from the `GithubStore`. The `GithubStore` is responsible for figuring out what message you're @@ -48,7 +48,7 @@ up or your package is manually activated. */ export function activate() { ComponentRegistry.register(ViewOnGithubButton, { - roles: ['message:Toolbar'], + role: 'MessageList:ThreadActionsToolbarButton', }); } diff --git a/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx b/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx index 07875c237..fade22ae0 100644 --- a/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx +++ b/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx @@ -14,8 +14,8 @@ display. Unlike a traditional React application, N1 components have very few guarantees on who will render them and where they will be rendered. In our `lib/main.cjsx` file we registered this component with our -{ComponentRegistry} for the `"message:Toolbar"` role. That means that -whenever the "message:Toolbar" region gets rendered, we'll render +{ComponentRegistry} for the `"ThreadActionsToolbarButton"` role. That means that +whenever the "ThreadActionsToolbarButton" region gets rendered, we'll render everything registered with that area. Other buttons, such as "Archive" and the "Change Label" button are reigstered with that role, so we should expect ourselves to showup alongside them. @@ -49,6 +49,8 @@ class ViewOnGithubButton extends React.Component @displayName: "ViewOnGithubButton" @containerRequired: false + @propTypes: + items: React.PropTypes.array #### React methods #### # The following methods are React methods that we override. See {React} @@ -76,9 +78,10 @@ class ViewOnGithubButton extends React.Component 'github:open': @_openLink render: -> + return null unless @props.items.length is 1 return null unless @state.link - - _onArchive: => + _onArchive: (event) => tasks = TaskFactory.tasksForArchiving - threads: @props.selection.items() + threads: @props.items Actions.queueTasks(tasks) + Actions.popSheet() + event.stopPropagation() return -class ThreadBulkTrashButton extends React.Component - @displayName: 'ThreadBulkTrashButton' +class TrashButton extends React.Component + @displayName: 'TrashButton' @containerRequired: false @propTypes: - selection: React.PropTypes.object.isRequired + items: React.PropTypes.array.isRequired render: -> - canTrashThreads = FocusedPerspectiveStore.current().canTrashThreads(@props.selection.items()) + canTrashThreads = FocusedPerspectiveStore.current().canTrashThreads(@props.items) return unless canTrashThreads - _onRemove: => + _onRemove: (event) => tasks = TaskFactory.tasksForMovingToTrash - threads: @props.selection.items() + threads: @props.items Actions.queueTasks(tasks) + Actions.popSheet() + event.stopPropagation() return -class ThreadBulkStarButton extends React.Component - @displayName: 'ThreadBulkStarButton' +class ToggleStarredButton extends React.Component + @displayName: 'ToggleStarredButton' @containerRequired: false @propTypes: - selection: React.PropTypes.object.isRequired + items: React.PropTypes.array.isRequired render: -> - postClickStarredState = _.every @props.selection.items(), (t) -> t.starred is false + postClickStarredState = _.every @props.items, (t) -> t.starred is false title = "Remove stars from all" imageName = "toolbar-star-selected.png" @@ -82,21 +86,22 @@ class ThreadBulkStarButton extends React.Component - _onStar: => - task = TaskFactory.taskForInvertingStarred(threads: @props.selection.items()) + _onStar: (event) => + task = TaskFactory.taskForInvertingStarred(threads: @props.items) Actions.queueTask(task) + event.stopPropagation() return -class ThreadBulkToggleUnreadButton extends React.Component - @displayName: 'ThreadBulkToggleUnreadButton' +class ToggleUnreadButton extends React.Component + @displayName: 'ToggleUnreadButton' @containerRequired: false @propTypes: - selection: React.PropTypes.object.isRequired + items: React.PropTypes.array.isRequired render: => - postClickUnreadState = _.every @props.selection.items(), (t) -> _.isMatch(t, {unread: false}) + postClickUnreadState = _.every @props.items, (t) -> _.isMatch(t, {unread: false}) fragment = if postClickUnreadState then "unread" else "read" - _onClick: => - task = TaskFactory.taskForInvertingUnread(threads: @props.selection.items()) + _onClick: (event) => + task = TaskFactory.taskForInvertingUnread(threads: @props.items) Actions.queueTask(task) + Actions.popSheet() + event.stopPropagation() return - ThreadNavButtonMixin = getInitialState: -> @_getStateFromStores() @@ -191,10 +197,10 @@ UpButton.containerRequired = false DownButton.containerRequired = false module.exports = { - DownButton, UpButton, - ThreadBulkArchiveButton, - ThreadBulkTrashButton, - ThreadBulkStarButton, - ThreadBulkToggleUnreadButton + DownButton, + TrashButton, + ArchiveButton, + ToggleStarredButton, + ToggleUnreadButton } diff --git a/internal_packages/thread-list/spec/thread-toolbar-buttons-spec.cjsx b/internal_packages/thread-list/spec/thread-toolbar-buttons-spec.cjsx new file mode 100644 index 000000000..58d9d5c1c --- /dev/null +++ b/internal_packages/thread-list/spec/thread-toolbar-buttons-spec.cjsx @@ -0,0 +1,65 @@ +React = require "react/addons" +ReactTestUtils = React.addons.TestUtils +TestUtils = React.addons.TestUtils +{Thread, FocusedContentStore, Actions, ChangeUnreadTask} = require "nylas-exports" +{ToggleStarredButton, ToggleUnreadButton} = require '../lib/thread-toolbar-buttons' + +test_thread = (new Thread).fromJSON({ + "id" : "thread_12345" + "account_id": TEST_ACCOUNT_ID + "subject" : "Subject 12345" + "starred": false +}) + +test_thread_starred = (new Thread).fromJSON({ + "id" : "thread_starred_12345" + "account_id": TEST_ACCOUNT_ID + "subject" : "Subject 12345" + "starred": true +}) + +describe "ThreadToolbarButtons", -> + + describe "Starring", -> + it "stars a thread if the star button is clicked and thread is unstarred", -> + spyOn(Actions, 'queueTask') + starButton = TestUtils.renderIntoDocument() + + TestUtils.Simulate.click React.findDOMNode(starButton) + + expect(Actions.queueTask.mostRecentCall.args[0].threads).toEqual([test_thread]) + expect(Actions.queueTask.mostRecentCall.args[0].starred).toEqual(true) + + it "unstars a thread if the star button is clicked and thread is starred", -> + spyOn(Actions, 'queueTask') + starButton = TestUtils.renderIntoDocument() + + TestUtils.Simulate.click React.findDOMNode(starButton) + + expect(Actions.queueTask.mostRecentCall.args[0].threads).toEqual([test_thread_starred]) + expect(Actions.queueTask.mostRecentCall.args[0].starred).toEqual(false) + + describe "Marking as unread", -> + thread = null + markUnreadBtn = null + + beforeEach -> + thread = new Thread(id: "thread-id-lol-123", accountId: TEST_ACCOUNT_ID, unread: false) + markUnreadBtn = ReactTestUtils.renderIntoDocument( + + ) + + it "queues a task to change unread status to true", -> + spyOn Actions, "queueTask" + ReactTestUtils.Simulate.click React.findDOMNode(markUnreadBtn).childNodes[0] + + changeUnreadTask = Actions.queueTask.calls[0].args[0] + expect(changeUnreadTask instanceof ChangeUnreadTask).toBe true + expect(changeUnreadTask.unread).toBe true + expect(changeUnreadTask.threads[0].id).toBe thread.id + + it "returns to the thread list", -> + spyOn Actions, "popSheet" + ReactTestUtils.Simulate.click React.findDOMNode(markUnreadBtn).childNodes[0] + + expect(Actions.popSheet).toHaveBeenCalled() diff --git a/internal_packages/thread-list/stylesheets/selected-items-stack.less b/internal_packages/thread-list/stylesheets/selected-items-stack.less new file mode 100644 index 000000000..00f70cd9a --- /dev/null +++ b/internal_packages/thread-list/stylesheets/selected-items-stack.less @@ -0,0 +1,53 @@ +@import "ui-variables"; +@img-path: "../internal_packages/thread-list/assets/graphic-stackable-card-filled.svg"; + +.selected-items-stack { + display: flex; + align-self: center; + align-items: center; + height: 100%; + + .selected-items-stack-content { + display: flex; + position: relative; + align-items: center; + justify-content: center; + width: 198px; + height: 268px; + + .stack { + .card { + position: absolute; + top: 0; + left: 0; + width: 198px; + height: 268px; + background: url(@img-path); + background-size: 198px 268px; + } + } + + .count-info { + display: flex; + flex-direction: column; + align-items: center; + z-index: 6; + + .count { + font-size: 4em; + font-weight: 200; + color: @text-color-very-subtle; + } + .count-message { + padding-top: @padding-base-vertical; + color: @text-color-very-subtle; + } + .clear { + padding-top: @padding-large-vertical * 2; + color: @text-color-link; + cursor: default; + } + } + } +} + diff --git a/internal_packages/thread-list/stylesheets/thread-list.less b/internal_packages/thread-list/stylesheets/thread-list.less index 64ebb7ea7..43dd8fc63 100644 --- a/internal_packages/thread-list/stylesheets/thread-list.less +++ b/internal_packages/thread-list/stylesheets/thread-list.less @@ -487,61 +487,3 @@ body.is-blurred { } } } - -@keyframes sending-progress-move { - 0% { - background-position: 0 0; - } - 100% { - background-position: 50px 50px; - } -} - -.draft-list { - .sending { - background-color: @background-primary; - &:hover { - background-color: @background-primary; - } - } - - .sending-progress { - display: block; - height:7px; - align-self: center; - background-color: @background-primary; - border-bottom:1px solid @border-color-divider; - position: relative; - - .filled { - display: block; - background: @component-active-color; - height:6px; - width: 0; //overridden by style - transition: width 1000ms linear; - } - .indeterminate { - display: block; - background: @component-active-color; - height:6px; - width: 100%; - } - .indeterminate:after { - content: ""; - position: absolute; - top: 0; left: 0; bottom: 0; right: 0; - background-image: linear-gradient( - -45deg, - rgba(255, 255, 255, .2) 25%, - transparent 25%, - transparent 50%, - rgba(255, 255, 255, .2) 50%, - rgba(255, 255, 255, .2) 75%, - transparent 75%, - transparent - ); - background-size: 50px 50px; - animation: sending-progress-move 2s linear infinite; - } - } -} diff --git a/internal_packages/thread-snooze/lib/main.js b/internal_packages/thread-snooze/lib/main.js index 25f6ce7e3..502a76440 100644 --- a/internal_packages/thread-snooze/lib/main.js +++ b/internal_packages/thread-snooze/lib/main.js @@ -1,6 +1,6 @@ /** @babel */ import {ComponentRegistry} from 'nylas-exports'; -import {ToolbarSnooze, BulkThreadSnooze, QuickActionSnooze} from './snooze-buttons'; +import {ToolbarSnooze, QuickActionSnooze} from './snooze-buttons'; import SnoozeMailLabel from './snooze-mail-label' import SnoozeStore from './snooze-store' @@ -9,16 +9,14 @@ export function activate() { this.snoozeStore = new SnoozeStore() this.snoozeStore.activate() - ComponentRegistry.register(ToolbarSnooze, {role: 'message:Toolbar'}); + ComponentRegistry.register(ToolbarSnooze, {role: 'ThreadActionsToolbarButton'}); ComponentRegistry.register(QuickActionSnooze, {role: 'ThreadListQuickAction'}); - ComponentRegistry.register(BulkThreadSnooze, {role: 'thread:BulkAction'}); ComponentRegistry.register(SnoozeMailLabel, {role: 'Thread:MailLabel'}); } export function deactivate() { ComponentRegistry.unregister(ToolbarSnooze); ComponentRegistry.unregister(QuickActionSnooze); - ComponentRegistry.unregister(BulkThreadSnooze); ComponentRegistry.unregister(SnoozeMailLabel); this.snoozeStore.deactivate() } diff --git a/internal_packages/thread-snooze/lib/snooze-buttons.jsx b/internal_packages/thread-snooze/lib/snooze-buttons.jsx index 755857394..0b2be045c 100644 --- a/internal_packages/thread-snooze/lib/snooze-buttons.jsx +++ b/internal_packages/thread-snooze/lib/snooze-buttons.jsx @@ -95,8 +95,8 @@ export class QuickActionSnooze extends Component { } -export class BulkThreadSnooze extends Component { - static displayName = 'BulkThreadSnooze'; +export class ToolbarSnooze extends Component { + static displayName = 'ToolbarSnooze'; static propTypes = { items: PropTypes.array, @@ -113,22 +113,3 @@ export class BulkThreadSnooze extends Component { ); } } - -export class ToolbarSnooze extends Component { - static displayName = 'ToolbarSnooze'; - - static propTypes = { - thread: PropTypes.object, - }; - - static containerRequired = false; - - render() { - if (!FocusedPerspectiveStore.current().isInbox()) { - return ; - } - return ( - - ); - } -} diff --git a/src/component-registry.coffee b/src/component-registry.coffee index a1e9d9140..a057ba89c 100644 --- a/src/component-registry.coffee +++ b/src/component-registry.coffee @@ -3,6 +3,13 @@ _ = require 'underscore' {Listener, Publisher} = require './flux/modules/reflux-coffee' CoffeeHelpers = require './flux/coffee-helpers' +DeprecatedRoles = { + 'thread:BulkAction': 'ThreadActionsToolbarButton', + 'draft:BulkAction': 'DraftActionsToolbarButton', + 'message:Toolbar': 'ThreadActionsToolbarButton', + 'thread:Toolbar': 'ThreadActionsToolbarButton', +} + ### Public: The ComponentRegistry maintains an index of React components registered by Nylas packages. Components can use {InjectedComponent} and {InjectedComponentSet} @@ -62,6 +69,8 @@ class ComponentRegistry if @_registry[component.displayName] and @_registry[component.displayName].component isnt component throw new Error("ComponentRegistry.register(): A different component was already registered with the name #{component.displayName}") + roles = @_removeDeprecatedRoles(component.displayName, roles) if roles + @_cache = {} @_registry[component.displayName] = {component, locations, modes, roles} @@ -153,6 +162,15 @@ class ComponentRegistry triggerDebounced: _.debounce(( -> @trigger(@)), 1) + _removeDeprecatedRoles: (displayName, roles) -> + newRoles = _.clone(roles) + roles.forEach (role, idx) -> + if role of DeprecatedRoles + instead = DeprecatedRoles[role] + console.warn("Deprecation warning! The role `#{role}` has been deprecated. + Register `#{displayName}` for the role `#{instead}` instead.") + newRoles.splice(idx, 1, instead) + return newRoles _pluralizeDescriptor: (descriptor) -> {locations, modes, roles} = descriptor diff --git a/internal_packages/thread-list/lib/empty-state.cjsx b/src/components/empty-list-state.cjsx similarity index 97% rename from internal_packages/thread-list/lib/empty-state.cjsx rename to src/components/empty-list-state.cjsx index 05fef5d5d..4ca2ec6a9 100644 --- a/internal_packages/thread-list/lib/empty-state.cjsx +++ b/src/components/empty-list-state.cjsx @@ -78,8 +78,8 @@ class InboxZero extends React.Component -class EmptyState extends React.Component - @displayName = 'EmptyState' +class EmptyListState extends React.Component + @displayName = 'EmptyListState' @propTypes = visible: React.PropTypes.bool.isRequired @@ -137,4 +137,4 @@ class EmptyState extends React.Component layoutMode: WorkspaceStore.layoutMode() syncing: NylasSyncStatusStore.busy() -module.exports = EmptyState +module.exports = EmptyListState diff --git a/internal_packages/thread-list/lib/focus-container.cjsx b/src/components/focus-container.cjsx similarity index 100% rename from internal_packages/thread-list/lib/focus-container.cjsx rename to src/components/focus-container.cjsx diff --git a/src/components/listens-to-observable.jsx b/src/components/listens-to-observable.jsx new file mode 100644 index 000000000..3e5c4c903 --- /dev/null +++ b/src/components/listens-to-observable.jsx @@ -0,0 +1,38 @@ +import React, {Component} from 'react' + +function ListensToObservable(ComposedComponent, {getObservable, getStateFromObservable}) { + return class extends Component { + static displayName = ComposedComponent.displayName; + + static containerRequired = ComposedComponent.containerRequired; + + constructor() { + super() + this.state = getStateFromObservable() + this.observable = getObservable() + } + + componentDidMount() { + this.unmounted = false + this.disposable = this.observable.subscribe(this.onObservableChanged) + } + + componentWillUnmount() { + this.unmounted = true + this.disposable.dispose() + } + + onObservableChanged = (data) => { + if (this.unmounted) return; + this.setState(getStateFromObservable(data)) + }; + + render() { + return ( + + ) + } + } +} + +export default ListensToObservable diff --git a/src/components/multiselect-action-bar.cjsx b/src/components/multiselect-action-bar.cjsx index 7a4ac666b..554596d0e 100644 --- a/src/components/multiselect-action-bar.cjsx +++ b/src/components/multiselect-action-bar.cjsx @@ -2,8 +2,7 @@ React = require "react/addons" _ = require 'underscore' {Utils, - Actions, - WorkspaceStore} = require "nylas-exports" + Actions} = require "nylas-exports" InjectedComponentSet = require './injected-component-set' TimeoutTransitionGroup = require './timeout-transition-group' RetinaImg = require './retina-img' @@ -32,7 +31,7 @@ collection name. To add an item to the bar created in the example above, registe ```coffee ComponentRegistry.register ThreadBulkTrashButton, - role: 'thread:BulkAction' + role: 'thread:Toolbar' ``` Section: Component Kit @@ -73,7 +72,6 @@ class MultiselectActionBar extends React.Component setupForProps: (props) => @_unsubscribers = [] - @_unsubscribers.push WorkspaceStore.listen @_onChange @_unsubscribers.push props.dataSource.listen @_onChange shouldComponentUpdate: (nextProps, nextState) => @@ -109,7 +107,7 @@ class MultiselectActionBar extends React.Component _renderActions: => return
unless @props.dataSource - _label: => diff --git a/src/components/multiselect-toolbar.jsx b/src/components/multiselect-toolbar.jsx new file mode 100644 index 000000000..5b1ba1b6a --- /dev/null +++ b/src/components/multiselect-toolbar.jsx @@ -0,0 +1,78 @@ +import {Utils} from 'nylas-exports' +import React, {Component, PropTypes} from 'react' +import TimeoutTransitionGroup from './timeout-transition-group' + + +/** + * MultiselectToolbar renders a toolbar inside a horizontal bar and displays + * a selection count and a button to clear the selection. + * + * The toolbar, or set of buttons, must be passed in as props.toolbarElement + * + * It will also animate its mounting and unmounting + * @class MultiselectToolbar + */ +class MultiselectToolbar extends Component { + static displayName = 'MultiselectToolbar'; + + static propTypes = { + toolbarElement: PropTypes.element.isRequired, + collection: PropTypes.string.isRequired, + onClearSelection: PropTypes.func.isRequired, + selectionCount: PropTypes.node, + }; + + shouldComponentUpdate(nextProps, nextState) { + return ( + !Utils.isEqualReact(nextProps, this.props) || + !Utils.isEqualReact(nextState, this.state) + ) + } + + selectionLabel = () => { + const {selectionCount, collection} = this.props + if (selectionCount > 1) { + return `${selectionCount} ${collection}s selected` + } else if (selectionCount === 1) { + return `${selectionCount} ${collection} selected` + } + return '' + }; + + renderToolbar() { + const {toolbarElement, onClearSelection} = this.props + return ( +
+
+ {toolbarElement} +
+ {this.selectionLabel()} +
+ + +
+
+ ) + } + + render() { + const {selectionCount} = this.props + return ( + + {selectionCount > 0 ? this.renderToolbar() : undefined} + + ) + } +} + +export default MultiselectToolbar diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee index cb97ae3b0..2da023d72 100644 --- a/src/global/nylas-component-kit.coffee +++ b/src/global/nylas-component-kit.coffee @@ -29,6 +29,8 @@ class NylasComponentKit @load "RetinaImg", 'retina-img' @load "SwipeContainer", 'swipe-container' @load "FluxContainer", 'flux-container' + @load "FocusContainer", 'focus-container' + @load "EmptyListState", 'empty-list-state' @load "ListTabular", 'list-tabular' @load "DraggableImg", 'draggable-img' @load "EventedIFrame", 'evented-iframe' @@ -38,7 +40,8 @@ class NylasComponentKit @load "KeyCommandsRegion", 'key-commands-region' @load "InjectedComponent", 'injected-component' @load "TokenizingTextField", 'tokenizing-text-field' - @load "MultiselectActionBar", 'multiselect-action-bar' + @loadDeprecated "MultiselectActionBar", 'multiselect-action-bar', instead: 'MultiselectToolbar' + @load "MultiselectToolbar", 'multiselect-toolbar' @load "InjectedComponentSet", 'injected-component-set' @load "TimeoutTransitionGroup", 'timeout-transition-group' @load "MetadataComposerToggleButton", 'metadata-composer-toggle-button' @@ -64,4 +67,7 @@ class NylasComponentKit @load "ScenarioEditor", 'scenario-editor' @load "NewsletterSignup", 'newsletter-signup' + # Higher order components + @load "ListensToObservable", 'listens-to-observable' + module.exports = new NylasComponentKit() diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee index 02b4aaa8d..af6293bc2 100644 --- a/src/global/nylas-exports.coffee +++ b/src/global/nylas-exports.coffee @@ -165,6 +165,7 @@ class NylasExports @load "PriorityUICoordinator", 'priority-ui-coordinator' # Utils + @load "DeprecateUtils", 'deprecate-utils' @load "Utils", 'flux/models/utils' @load "DOMUtils", 'dom-utils' @load "VirtualDOMUtils", 'virtual-dom-utils' diff --git a/src/global/nylas-observables.coffee b/src/global/nylas-observables.coffee index ddf1a09b6..d046031d1 100644 --- a/src/global/nylas-observables.coffee +++ b/src/global/nylas-observables.coffee @@ -67,6 +67,33 @@ Rx.Observable.fromStore = (store) => observer.onNext(store) return Rx.Disposable.create(unsubscribe) +# Takes a store that provides an {ObservableListDataSource} via `dataSource()` +# Returns an observable that provides array of selected items on subscription +Rx.Observable.fromListSelection = (originStore) => + return Rx.Observable.create((observer) => + dataSourceDisposable = null + storeObservable = Rx.Observable.fromStore(originStore) + + disposable = storeObservable.subscribe( => + dataSource = originStore.dataSource() + dataSourceObservable = Rx.Observable.fromStore(dataSource) + + if dataSourceDisposable + dataSourceDisposable.dispose() + + dataSourceDisposable = dataSourceObservable.subscribe( => + observer.onNext(dataSource.selection.items()) + ) + return + ) + return { + dispose: => + if dataSourceDisposable + dataSourceDisposable.dispose() + disposable.dispose() + } + ) + Rx.Observable.fromConfig = (configKey) => return Rx.Observable.create (observer) => disposable = NylasEnv.config.onDidChange configKey, => diff --git a/internal_packages/thread-list/stylesheets/empty-state.less b/static/components/empty-list-state.less similarity index 100% rename from internal_packages/thread-list/stylesheets/empty-state.less rename to static/components/empty-list-state.less diff --git a/static/index.less b/static/index.less index 5833e4cfe..ece819d96 100644 --- a/static/index.less +++ b/static/index.less @@ -33,3 +33,4 @@ @import "components/fixed-popover"; @import "components/modal"; @import "components/date-input"; +@import "components/empty-list-state";