mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 15:56:10 +08:00
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:
parent
b5fe01e5d0
commit
e83bf2bbec
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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'}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
}
|
||||
}
|
|
@ -18,6 +18,9 @@ class DraftListStore extends NylasStore
|
|||
dataSource: =>
|
||||
@_dataSource
|
||||
|
||||
selectionObservable: =>
|
||||
return Rx.Observable.fromListSelection(@)
|
||||
|
||||
# Inbound Events
|
||||
|
||||
_onPerspectiveChanged: =>
|
50
internal_packages/draft-list/lib/draft-list-toolbar.jsx
Normal file
50
internal_packages/draft-list/lib/draft-list-toolbar.jsx
Normal 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})
|
|
@ -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}
|
|
@ -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"
|
||||
|
27
internal_packages/draft-list/lib/main.es6
Normal file
27
internal_packages/draft-list/lib/main.es6
Normal 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)
|
||||
}
|
13
internal_packages/draft-list/package.json
Executable file
13
internal_packages/draft-list/package.json
Executable 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": {
|
||||
}
|
||||
}
|
61
internal_packages/draft-list/stylesheets/draft-list.less
Normal file
61
internal_packages/draft-list/stylesheets/draft-list.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
.btn.btn-toolbar.btn-view-on-github {
|
||||
&:only-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
61
internal_packages/thread-list/lib/message-list-toolbar.jsx
Normal file
61
internal_packages/thread-list/lib/message-list-toolbar.jsx
Normal 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)
|
73
internal_packages/thread-list/lib/selected-items-stack.jsx
Normal file
73
internal_packages/thread-list/lib/selected-items-stack.jsx
Normal 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})
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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: =>
|
||||
|
|
39
internal_packages/thread-list/lib/thread-list-toolbar.jsx
Normal file
39
internal_packages/thread-list/lib/thread-list-toolbar.jsx
Normal 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)
|
|
@ -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}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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()
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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]}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
38
src/components/listens-to-observable.jsx
Normal file
38
src/components/listens-to-observable.jsx
Normal 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
|
|
@ -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: =>
|
||||
|
|
78
src/components/multiselect-toolbar.jsx
Normal file
78
src/components/multiselect-toolbar.jsx
Normal 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
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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, =>
|
||||
|
|
|
@ -33,3 +33,4 @@
|
|||
@import "components/fixed-popover";
|
||||
@import "components/modal";
|
||||
@import "components/date-input";
|
||||
@import "components/empty-list-state";
|
||||
|
|
Loading…
Reference in a new issue