feat(selection): Add new display for selection count + update toolbar

Summary:
- New behavior is that the in split mode, you will perform actions on
  the selection via the MessageListToolbar (the toolbar positioned above
  the message list)
- Refactored and moved around a bunch of code to achieve this:
  - Mostly renaming stuff and moving stuff around and removing some
    duplication
  - Update naming of toolbar role to a single role, and update relevant code
  - Converted and refactored a bunch of code into ES6, specifically to reuse the code for the ThreadActionsToolbar at the 2 locations
  - Deprecated MultiselectActionBar in favor of MultiselectToolbar
  - Deprecated old roles
- Punted the animation for the stackable cards in the selection display for now.
- #370

Test Plan: - Manual and unit tests

Reviewers: evan, drew, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2756
This commit is contained in:
Juan Tejada 2016-03-17 09:50:30 -07:00
parent b5fe01e5d0
commit e83bf2bbec
51 changed files with 832 additions and 396 deletions

View file

@ -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()
};

View file

@ -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(
<CategoryPickerPopover
threads={@_threads}
threads={@props.items}
account={@_account} />,
{originRect: buttonRect, direction: 'down'}
)

View file

@ -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)

View file

@ -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 <span className="no-subject">(No Subject)</span>
else
return subj
ParticipantsColumn = new ListTabular.Column
name: "Participants"
width: 200

View file

@ -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 (
<Flexbox style={{width: 150, whiteSpace: 'no-wrap'}}>
<Flexbox style={{width: 150, whiteSpace: 'nowrap'}}>
<SendingProgressBar
style={{flex: 1, marginRight: 10}}
progress={draft.uploadProgress * 100}
@ -24,6 +24,6 @@ export default class DraftListSendStatus extends Component {
</Flexbox>
)
}
return <span className="timestamp">{timestamp(draft.date)}</span>
return <span className="timestamp">{Utils.shortTimeString(draft.date)}</span>
}
}

View file

@ -18,6 +18,9 @@ class DraftListStore extends NylasStore
dataSource: =>
@_dataSource
selectionObservable: =>
return Rx.Observable.fromListSelection(@)
# Inbound Events
_onPerspectiveChanged: =>

View file

@ -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 = (
<InjectedComponentSet
matching={{role: "DraftActionsToolbarButton"}}
exposedProps={{selection, items}} />
)
return (
<MultiselectToolbar
collection="draft"
selectionCount={items.length}
toolbarElement={toolbarElement}
onClearSelection={this.onClearSelection}
/>
)
}
}
export default ListensToObservable(DraftListToolbar, {getObservable, getStateFromObservable})

View file

@ -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
<MultiselectList
columns={DraftListColumns.Wide}
onDoubleClick={@_onDoubleClick}
emptyComponent={EmptyState}
emptyComponent={EmptyListState}
keymapHandlers={@_keymapHandlers()}
itemPropsProvider={@_itemPropsProvider}
itemHeight={39}

View file

@ -1,5 +1,4 @@
React = require "react/addons"
classNames = require 'classnames'
React = require "react"
{RetinaImg} = require 'nylas-component-kit'
{Actions, FocusedContentStore, DestroyDraftTask} = require "nylas-exports"

View file

@ -0,0 +1,27 @@
import {WorkspaceStore, ComponentRegistry} from 'nylas-exports'
import DraftList from './draft-list'
import DraftListToolbar from './draft-list-toolbar'
import DraftListSendStatus from './draft-list-send-status'
import {DraftDeleteButton} from "./draft-toolbar-buttons"
export function activate() {
WorkspaceStore.defineSheet(
'Drafts',
{root: true},
{list: ['RootSidebar', 'DraftList']}
)
ComponentRegistry.register(DraftList, {location: WorkspaceStore.Location.DraftList})
ComponentRegistry.register(DraftListToolbar, {location: WorkspaceStore.Location.DraftList.Toolbar})
ComponentRegistry.register(DraftDeleteButton, {role: 'DraftActionsToolbarButton'})
ComponentRegistry.register(DraftListSendStatus, {role: 'DraftList:DraftStatus'})
}
export function deactivate() {
ComponentRegistry.unregister(DraftList)
ComponentRegistry.unregister(DraftListToolbar)
ComponentRegistry.unregister(DraftDeleteButton)
ComponentRegistry.unregister(DraftListSendStatus)
}

View file

@ -0,0 +1,13 @@
{
"name": "draft-list",
"version": "0.1.0",
"main": "./lib/main",
"description": "View drafts using React",
"license": "GPL-3.0",
"private": true,
"engines": {
"nylas": "*"
},
"dependencies": {
}
}

View file

@ -0,0 +1,61 @@
@import "ui-variables";
@import "ui-mixins";
@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;
}
}
}

View file

@ -1,48 +1,27 @@
MessageList = require "./message-list"
MessageListHiddenMessagesToggle = require './message-list-hidden-messages-toggle'
MessageToolbarItems = require "./message-toolbar-items"
{ComponentRegistry,
ExtensionRegistry,
WorkspaceStore} = require 'nylas-exports'
MessageList = require "./message-list"
MessageListHiddenMessagesToggle = require './message-list-hidden-messages-toggle'
SidebarPluginContainer = require "./sidebar-plugin-container"
SidebarParticipantPicker = require './sidebar-participant-picker'
ThreadStarButton = require './thread-star-button'
ThreadArchiveButton = require './thread-archive-button'
ThreadTrashButton = require './thread-trash-button'
ThreadToggleUnreadButton = require './thread-toggle-unread-button'
TrackingPixelsExtension = require './plugins/tracking-pixels-extension'
module.exports =
item: null # The DOM item the main React component renders into
activate: (@state={}) ->
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

View file

@ -187,7 +187,7 @@ class MessageList extends React.Component
render: =>
if not @state.currentThread
return <div className="message-list" id="message-list"></div>
return <span />
wrapClass = classNames
"messages-wrap": true

View file

@ -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: =>
<TimeoutTransitionGroup
className="message-toolbar-items"
leaveTimeout={125}
enterTimeout={125}
transitionName="opacity-125ms">
{@_renderContents()}
</TimeoutTransitionGroup>
_renderContents: =>
return false unless @state.thread
<InjectedComponentSet key="injected" matching={role: "message:Toolbar"} exposedProps={thread: @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

View file

@ -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(<StarButton thread={test_thread}/>)
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(<StarButton thread={test_thread_starred}/>)
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(
<ThreadToggleUnreadButton thread={thread} />
)
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()

View file

@ -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',
});
}

View file

@ -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
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
<button className="btn btn-toolbar"
<button className="btn btn-toolbar btn-view-on-github"
onClick={@_openLink}
title={"Visit Thread on GitHub"}>
<RetinaImg

View file

@ -0,0 +1,6 @@
.btn.btn-toolbar.btn-view-on-github {
&:only-of-type {
margin-right: 0;
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="390px" height="527px" viewBox="0 0 390 527" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.6.1 (26313) - http://www.bohemiancoding.com/sketch -->
<title>Rectangle 2</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle-2" stroke="#E2E2E2" stroke-width="2.5" fill="#FFFFFF" x="0" y="0" width="390" height="527" rx="15"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 608 B

View file

@ -1,17 +0,0 @@
React = require "react/addons"
DraftListStore = require './draft-list-store'
{MultiselectActionBar, FluxContainer} = require 'nylas-component-kit'
class DraftSelectionBar extends React.Component
@displayName: 'DraftSelectionBar'
render: =>
<FluxContainer
stores={[DraftListStore]}
getStateFromStores={ -> dataSource: DraftListStore.dataSource() }>
<MultiselectActionBar
className="draft-list"
collection="draft" />
</FluxContainer>
module.exports = DraftSelectionBar

View file

@ -1,13 +0,0 @@
{Utils} = require 'nylas-exports'
React = require 'react'
module.exports =
timestamp: (time) ->
Utils.shortTimeString(time)
subject: (subj) ->
if (subj ? "").trim().length is 0
return <span className="no-subject">(No Subject)</span>
else
return subj

View file

@ -0,0 +1,63 @@
import React, {Component, PropTypes} from 'react'
import {ListensToObservable, InjectedComponentSet} from 'nylas-component-kit'
import ThreadListStore from './thread-list-store'
export const ToolbarRole = 'ThreadActionsToolbarButton'
function defaultObservable() {
return ThreadListStore.selectionObservable()
}
function InjectsToolbarButtons(ToolbarComponent, {getObservable, extraRoles = []}) {
const roles = [ToolbarRole].concat(extraRoles)
class ComposedComponent extends Component {
static displayName = ToolbarComponent.displayName;
static propTypes = {
items: PropTypes.array,
};
static containerRequired = false;
render() {
const {items} = this.props;
const {selection} = ThreadListStore.dataSource()
// Keep all of the exposed props from deprecated regions that now map to this one
const exposedProps = {
items,
selection,
thread: items[0],
}
const injectedButtons = (
<InjectedComponentSet
key="injected"
matching={{roles}}
exposedProps={exposedProps} />
)
return (
<ToolbarComponent
items={items}
selection={selection}
injectedButtons={injectedButtons}
/>
)
}
}
const getStateFromObservable = (items) => {
if (!items) {
return {items: []}
}
return {items}
}
return ListensToObservable(ComposedComponent, {
getObservable: getObservable || defaultObservable,
getStateFromObservable,
})
}
export default InjectsToolbarButtons

View file

@ -2,32 +2,34 @@ _ = require 'underscore'
React = require "react"
{ComponentRegistry, WorkspaceStore} = require "nylas-exports"
{DownButton, UpButton, ThreadBulkArchiveButton, ThreadBulkTrashButton,
ThreadBulkStarButton, ThreadBulkToggleUnreadButton} = require "./thread-buttons"
{DraftDeleteButton} = require "./draft-buttons"
ThreadSelectionBar = require './thread-selection-bar'
ThreadList = require './thread-list'
ThreadListToolbar = require './thread-list-toolbar'
MessageListToolbar = require './message-list-toolbar'
SelectedItemsStack = require './selected-items-stack'
DraftSelectionBar = require './draft-selection-bar'
DraftList = require './draft-list'
DraftListSendStatus = require './draft-list-send-status'
{UpButton,
DownButton,
TrashButton,
ArchiveButton,
ToggleUnreadButton,
ToggleStarredButton} = require "./thread-toolbar-buttons"
module.exports =
activate: (@state={}) ->
WorkspaceStore.defineSheet 'Drafts', {root: true},
list: ['RootSidebar', 'DraftList']
ComponentRegistry.register ThreadList,
location: WorkspaceStore.Location.ThreadList
ComponentRegistry.register ThreadSelectionBar,
ComponentRegistry.register SelectedItemsStack,
location: WorkspaceStore.Location.MessageList
modes: ['split']
# Toolbars
ComponentRegistry.register ThreadListToolbar,
location: WorkspaceStore.Location.ThreadList.Toolbar
modes: ['list']
ComponentRegistry.register DraftList,
location: WorkspaceStore.Location.DraftList
ComponentRegistry.register DraftSelectionBar,
location: WorkspaceStore.Location.DraftList.Toolbar
ComponentRegistry.register MessageListToolbar,
location: WorkspaceStore.Location.MessageList.Toolbar
ComponentRegistry.register DownButton,
location: WorkspaceStore.Location.MessageList.Toolbar
@ -37,33 +39,26 @@ module.exports =
location: WorkspaceStore.Location.MessageList.Toolbar
modes: ['list']
ComponentRegistry.register ThreadBulkArchiveButton,
role: 'thread:BulkAction'
ComponentRegistry.register ArchiveButton,
role: 'ThreadActionsToolbarButton'
ComponentRegistry.register ThreadBulkTrashButton,
role: 'thread:BulkAction'
ComponentRegistry.register TrashButton,
role: 'ThreadActionsToolbarButton'
ComponentRegistry.register ThreadBulkStarButton,
role: 'thread:BulkAction'
ComponentRegistry.register ToggleStarredButton,
role: 'ThreadActionsToolbarButton'
ComponentRegistry.register ThreadBulkToggleUnreadButton,
role: 'thread:BulkAction'
ComponentRegistry.register DraftDeleteButton,
role: 'draft:BulkAction'
ComponentRegistry.register DraftListSendStatus,
role: 'DraftList:DraftStatus'
ComponentRegistry.register ToggleUnreadButton,
role: 'ThreadActionsToolbarButton'
deactivate: ->
ComponentRegistry.unregister DraftList
ComponentRegistry.unregister DraftSelectionBar
ComponentRegistry.unregister ThreadList
ComponentRegistry.unregister ThreadSelectionBar
ComponentRegistry.unregister ThreadBulkArchiveButton
ComponentRegistry.unregister ThreadBulkTrashButton
ComponentRegistry.unregister ThreadBulkToggleUnreadButton
ComponentRegistry.unregister DownButton
ComponentRegistry.unregister SelectedItemsStack
ComponentRegistry.unregister ThreadListToolbar
ComponentRegistry.unregister MessageListToolbar
ComponentRegistry.unregister ArchiveButton
ComponentRegistry.unregister TrashButton
ComponentRegistry.unregister ToggleUnreadButton
ComponentRegistry.unregister ToggleStarredButton
ComponentRegistry.unregister UpButton
ComponentRegistry.unregister DraftDeleteButton
ComponentRegistry.unregister DraftListSendStatus
ComponentRegistry.unregister DownButton

View file

@ -0,0 +1,61 @@
import Rx from 'rx-lite'
import React, {Component, PropTypes} from 'react'
import {FocusedContentStore} from 'nylas-exports'
import {TimeoutTransitionGroup} from 'nylas-component-kit'
import ThreadListStore from './thread-list-store'
import InjectsToolbarButtons, {ToolbarRole} from './injects-toolbar-buttons'
function getObservable() {
return (
Rx.Observable.merge(
Rx.Observable.fromStore(FocusedContentStore),
ThreadListStore.selectionObservable(),
)
.map((data) => {
const storeChanged = data === FocusedContentStore
const selectionChanged = data instanceof Array
if (storeChanged) {
const focusedThread = FocusedContentStore.focused('thread')
if (focusedThread) {
return [focusedThread]
}
} else if (selectionChanged) {
return data
}
return []
})
)
}
class MessageListToolbar extends Component {
static displayName = 'MessageListToolbar';
static propTypes = {
items: PropTypes.array,
injectedButtons: PropTypes.element,
};
render() {
const {items, injectedButtons} = this.props
const shouldRender = items.length > 0
return (
<TimeoutTransitionGroup
className="message-toolbar-items"
leaveTimeout={125}
enterTimeout={125}
transitionName="opacity-125ms">
{shouldRender ? injectedButtons : undefined}
</TimeoutTransitionGroup>
)
}
}
const toolbarProps = {
getObservable,
extraRoles: [`MessageList:${ToolbarRole}`],
}
export default InjectsToolbarButtons(MessageListToolbar, toolbarProps)

View file

@ -0,0 +1,73 @@
import _ from 'underscore'
import React, {Component, PropTypes} from 'react'
import {ListensToObservable} from 'nylas-component-kit'
import ThreadListStore from './thread-list-store'
function getObservable() {
return (
ThreadListStore.selectionObservable()
.map(items => items.length)
)
}
function getStateFromObservable(selectionCount) {
if (!selectionCount) {
return {selectionCount: 0}
}
return {selectionCount}
}
class SelectedItemsStack extends Component {
static displayName = "SelectedItemsStack";
static propTypes = {
selectionCount: PropTypes.number,
};
onClearSelection = ()=> {
ThreadListStore.dataSource().selection.clear()
};
static containerRequired = false;
render() {
const {selectionCount} = this.props
if (selectionCount <= 1) {
return <span />
}
const cardCount = Math.min(5, selectionCount)
return (
<div className="selected-items-stack">
<div className="selected-items-stack-content">
<div className="stack">
{_.times(cardCount, (idx) => {
let deg = idx * 0.9;
if (idx === 1) {
deg += 0.5
}
let transform = `rotate(${deg}deg)`
if (idx === cardCount - 1) {
transform += ' translate(2px, 3px)'
}
const style = {
transform,
zIndex: 5 - idx,
}
return <div style={style} className="card"/>
})}
</div>
<div className="count-info">
<div className="count">{selectionCount}</div>
<div className="count-message">messages selected</div>
<div className="clear" onClick={this.onClearSelection}>clear selection</div>
</div>
</div>
</div>
)
}
}
export default ListensToObservable(SelectedItemsStack, {getObservable, getStateFromObservable})

View file

@ -8,23 +8,26 @@ classNames = require 'classnames'
MailImportantIcon,
InjectedComponentSet} = require 'nylas-component-kit'
{Thread, FocusedPerspectiveStore} = require 'nylas-exports'
{Thread, FocusedPerspectiveStore, Utils} = require 'nylas-exports'
{ThreadArchiveQuickAction,
ThreadTrashQuickAction} = require './thread-list-quick-actions'
{timestamp,
subject} = require './formatting-utils'
ThreadListParticipants = require './thread-list-participants'
ThreadListStore = require './thread-list-store'
ThreadListIcon = require './thread-list-icon'
TimestampComponentForPerspective = (thread) ->
if FocusedPerspectiveStore.current().isSent()
<span className="timestamp">{timestamp(thread.lastMessageSentTimestamp)}</span>
<span className="timestamp">{Utils.shortTimeString(thread.lastMessageSentTimestamp)}</span>
else
<span className="timestamp">{timestamp(thread.lastMessageReceivedTimestamp)}</span>
<span className="timestamp">{Utils.shortTimeString(thread.lastMessageReceivedTimestamp)}</span>
subject = (subj) ->
if (subj ? "").trim().length is 0
return <span className="no-subject">(No Subject)</span>
else
return subj
c1 = new ListTabular.Column

View file

@ -1,7 +1,6 @@
React = require 'react'
{Utils} = require 'nylas-exports'
ThreadListStore = require './thread-list-store'
{timestamp} = require './formatting-utils'
class ThreadListScrollTooltip extends React.Component
@displayName: 'ThreadListScrollTooltip'
@ -26,7 +25,7 @@ class ThreadListScrollTooltip extends React.Component
render: ->
if @state.item
content = timestamp(@state.item.lastMessageReceivedTimestamp)
content = Utils.shortTimeString(@state.item.lastMessageReceivedTimestamp)
else
content = "Loading..."
<div className="scroll-tooltip">

View file

@ -1,7 +1,8 @@
_ = require 'underscore'
NylasStore = require 'nylas-store'
{Thread,
{Rx,
Thread,
Message,
Actions,
DatabaseStore,
@ -42,6 +43,9 @@ class ThreadListStore extends NylasStore
@trigger(@)
Actions.setFocus(collection: 'thread', item: null)
selectionObservable: =>
return Rx.Observable.fromListSelection(@)
# Inbound Events
_onPerspectiveChanged: =>

View file

@ -0,0 +1,39 @@
import React, {Component, PropTypes} from 'react'
import {MultiselectToolbar} from 'nylas-component-kit'
import InjectsToolbarButtons, {ToolbarRole} from './injects-toolbar-buttons'
class ThreadListToolbar extends Component {
static displayName = 'ThreadListToolbar';
static propTypes = {
items: PropTypes.array,
selection: PropTypes.shape({
clear: PropTypes.func,
}),
injectedButtons: PropTypes.element,
};
onClearSelection = ()=> {
this.props.selection.clear()
};
render() {
const {injectedButtons, items} = this.props
return (
<MultiselectToolbar
collection="thread"
selectionCount={items.length}
toolbarElement={injectedButtons}
onClearSelection={this.onClearSelection}
/>
)
}
}
const toolbarProps = {
extraRoles: [`ThreadList:${ToolbarRole}`],
}
export default InjectsToolbarButtons(ThreadListToolbar, toolbarProps)

View file

@ -2,7 +2,10 @@ _ = require 'underscore'
React = require 'react'
classNames = require 'classnames'
{MultiselectList, FluxContainer} = require 'nylas-component-kit'
{MultiselectList,
FocusContainer,
EmptyListState,
FluxContainer} = require 'nylas-component-kit'
{Actions,
Thread,
@ -20,8 +23,6 @@ classNames = require 'classnames'
ThreadListColumns = require './thread-list-columns'
ThreadListScrollTooltip = require './thread-list-scroll-tooltip'
ThreadListStore = require './thread-list-store'
FocusContainer = require './focus-container'
EmptyState = require './empty-state'
ThreadListContextMenu = require './thread-list-context-menu'
CategoryRemovalTargetRulesets = require './category-removal-target-rulesets'
@ -96,7 +97,7 @@ class ThreadList extends React.Component
itemHeight={itemHeight}
className="thread-list thread-list-#{@state.style}"
scrollTooltipComponent={ThreadListScrollTooltip}
emptyComponent={EmptyState}
emptyComponent={EmptyListState}
keymapHandlers={@_keymapHandlers()}
onDragStart={@_onDragStart}
onDragEnd={@_onDragEnd}

View file

@ -1,17 +0,0 @@
React = require "react/addons"
ThreadListStore = require './thread-list-store'
{MultiselectActionBar, FluxContainer} = require 'nylas-component-kit'
class ThreadSelectionBar extends React.Component
@displayName: 'ThreadSelectionBar'
render: =>
<FluxContainer
stores={[ThreadListStore]}
getStateFromStores={ -> dataSource: ThreadListStore.dataSource() }>
<MultiselectActionBar
className="thread-list"
collection="thread" />
</FluxContainer>
module.exports = ThreadSelectionBar

View file

@ -10,15 +10,15 @@ ThreadListStore = require './thread-list-store'
FocusedContentStore,
FocusedPerspectiveStore} = require "nylas-exports"
class ThreadBulkArchiveButton extends React.Component
@displayName: 'ThreadBulkArchiveButton'
class ArchiveButton extends React.Component
@displayName: 'ArchiveButton'
@containerRequired: false
@propTypes:
selection: React.PropTypes.object.isRequired
items: React.PropTypes.array.isRequired
render: ->
canArchiveThreads = FocusedPerspectiveStore.current().canArchiveThreads(@props.selection.items())
canArchiveThreads = FocusedPerspectiveStore.current().canArchiveThreads(@props.items)
return <span /> unless canArchiveThreads
<button style={order:-107}
@ -28,21 +28,23 @@ class ThreadBulkArchiveButton extends React.Component
<RetinaImg name="toolbar-archive.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
_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 <span /> unless canTrashThreads
<button style={order:-106}
@ -52,22 +54,24 @@ class ThreadBulkTrashButton extends React.Component
<RetinaImg name="toolbar-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
_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
<RetinaImg name={imageName} mode={RetinaImg.Mode.ContentIsMask} />
</button>
_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"
<button style={order:-105}
@ -107,12 +112,13 @@ class ThreadBulkToggleUnreadButton extends React.Component
mode={RetinaImg.Mode.ContentIsMask} />
</button>
_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
}

View file

@ -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(<ToggleStarredButton items={[test_thread]}/>)
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(<ToggleStarredButton items={[test_thread_starred]}/>)
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(
<ToggleUnreadButton items={[thread]} />
)
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()

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}
}

View file

@ -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()
}

View file

@ -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 <span />;
}
return (
<SnoozeButton threads={[this.props.thread]}/>
);
}
}

View file

@ -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

View file

@ -78,8 +78,8 @@ class InboxZero extends React.Component
</div>
</div>
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

View file

@ -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 (
<ComposedComponent {...this.state} {...this.props} />
)
}
}
}
export default ListensToObservable

View file

@ -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 <div></div> unless @props.dataSource
<InjectedComponentSet matching={role:"#{@props.collection}:BulkAction"}
<InjectedComponentSet matching={role:"#{@props.collection}:Toolbar"}
exposedProps={selection: @props.dataSource.selection, items: @state.items} />
_label: =>

View file

@ -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 (
<div className="absolute" key="absolute">
<div className="inner">
{toolbarElement}
<div className="centered">
{this.selectionLabel()}
</div>
<button
style={{order: 100}}
className="btn btn-toolbar"
onClick={onClearSelection}>
Clear Selection
</button>
</div>
</div>
)
}
render() {
const {selectionCount} = this.props
return (
<TimeoutTransitionGroup
className={"selection-bar"}
transitionName="selection-bar-absolute"
component="div"
leaveTimeout={200}
enterTimeout={200}>
{selectionCount > 0 ? this.renderToolbar() : undefined}
</TimeoutTransitionGroup>
)
}
}
export default MultiselectToolbar

View file

@ -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()

View file

@ -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'

View file

@ -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, =>

View file

@ -33,3 +33,4 @@
@import "components/fixed-popover";
@import "components/modal";
@import "components/date-input";
@import "components/empty-list-state";