mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-29 11:52:34 +08:00
Fix drafts
This commit is contained in:
parent
d008d5f475
commit
22121f9f18
20 changed files with 325 additions and 328 deletions
|
@ -2,7 +2,6 @@ _ = require 'underscore'
|
|||
{WorkspaceStore,
|
||||
MailboxPerspective,
|
||||
FocusedPerspectiveStore,
|
||||
DraftCountStore,
|
||||
DestroyCategoryTask,
|
||||
Actions} = require 'nylas-exports'
|
||||
{OutlineViewItem} = require 'nylas-component-kit'
|
||||
|
@ -18,7 +17,7 @@ countForItem = (perspective) ->
|
|||
return 0
|
||||
|
||||
isItemSelected = (perspective) ->
|
||||
(WorkspaceStore.rootSheet() is WorkspaceStore.Sheet.Threads and
|
||||
(WorkspaceStore.rootSheet() in [WorkspaceStore.Sheet.Threads, WorkspaceStore.Sheet.Drafts] and
|
||||
FocusedPerspectiveStore.current().isEqual(perspective))
|
||||
|
||||
isItemDeleted = (perspective) ->
|
||||
|
@ -36,31 +35,29 @@ toggleItemCollapsed = (item) ->
|
|||
|
||||
class SidebarItem
|
||||
|
||||
@forPerspective: (id, perspective, {children, deletable, name} = {}) ->
|
||||
children ?= []
|
||||
@forPerspective: (id, perspective, opts = {}) ->
|
||||
counterStyle = OutlineViewItem.CounterStyles.Alt if perspective.isInbox()
|
||||
dataTransferType = 'nylas-thread-ids'
|
||||
|
||||
if deletable
|
||||
onDeleteItem = (item) ->
|
||||
if opts.deletable
|
||||
onDeleteItem = (item) ->
|
||||
# TODO Delete multiple categories at once
|
||||
return if item.perspective.categories.length > 1
|
||||
return if item.deleted is true
|
||||
category = item.perspective.categories[0]
|
||||
Actions.queueTask(new DestroyCategoryTask({category: category}))
|
||||
|
||||
return {
|
||||
return _.extend({
|
||||
id: id
|
||||
name: name ? perspective.name
|
||||
name: perspective.name
|
||||
count: countForItem(perspective)
|
||||
iconName: perspective.iconName
|
||||
children: children
|
||||
children: []
|
||||
perspective: perspective
|
||||
selected: isItemSelected(perspective)
|
||||
collapsed: isItemCollapsed(id) ? true
|
||||
deleted: isItemDeleted(perspective)
|
||||
counterStyle: counterStyle
|
||||
dataTransferType: dataTransferType
|
||||
dataTransferType: 'nylas-thread-ids'
|
||||
onDelete: onDeleteItem
|
||||
onToggleCollapsed: toggleItemCollapsed
|
||||
onDrop: (item, ids) ->
|
||||
|
@ -75,7 +72,7 @@ class SidebarItem
|
|||
onSelect: (item) ->
|
||||
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
|
||||
Actions.focusMailboxPerspective(item.perspective)
|
||||
}
|
||||
}, opts)
|
||||
|
||||
|
||||
@forCategories: (categories = [], opts = {}) ->
|
||||
|
@ -89,30 +86,12 @@ class SidebarItem
|
|||
id += "-#{opts.name}" if opts.name
|
||||
@forPerspective(id, perspective, opts)
|
||||
|
||||
@forSheet: (id, name, iconName, sheet, count, collapsed, children = []) ->
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
iconName,
|
||||
count,
|
||||
sheet,
|
||||
children,
|
||||
collapsed: isItemCollapsed(id) ? true
|
||||
onToggleCollapsed: toggleItemCollapsed
|
||||
onSelect: (item) ->
|
||||
Actions.selectRootSheet(item.sheet)
|
||||
}
|
||||
|
||||
@forDrafts: ({accountId, name, children, collapsed} = {}) ->
|
||||
id = 'Drafts'
|
||||
id += "-#{name}" if name
|
||||
sheet = WorkspaceStore.Sheet.Drafts
|
||||
iconName = 'drafts.png'
|
||||
count = if accountId?
|
||||
DraftCountStore.count(accountId)
|
||||
else
|
||||
DraftCountStore.totalCount()
|
||||
@forSheet(id, name ? id, iconName, sheet, count, collapsed, children)
|
||||
|
||||
@forDrafts: (accountIds, opts = {}) ->
|
||||
perspective = MailboxPerspective.forDrafts(accountIds)
|
||||
id = "Drafts-#{opts.name}"
|
||||
opts.onSelect = ->
|
||||
Actions.focusMailboxPerspective(perspective)
|
||||
Actions.selectRootSheet(WorkspaceStore.Sheet.Drafts)
|
||||
@forPerspective(id, perspective, opts)
|
||||
|
||||
module.exports = SidebarItem
|
||||
|
|
|
@ -74,9 +74,8 @@ class SidebarSection
|
|||
starredItem = SidebarItem.forStarred(_.pluck(accounts, 'id'),
|
||||
children: accounts.map (acc) -> SidebarItem.forStarred([acc.id], name: acc.label)
|
||||
)
|
||||
draftsItem = SidebarItem.forDrafts(
|
||||
children: accounts.map (acc) ->
|
||||
SidebarItem.forDrafts(accountId: acc.id, name: acc.label)
|
||||
draftsItem = SidebarItem.forDrafts(_.pluck(accounts, 'id'),
|
||||
children: accounts.map (acc) -> SidebarItem.forDrafts([acc.id], name: acc.label)
|
||||
)
|
||||
|
||||
# Order correctly: Inbox, Starred, rest... , Drafts
|
||||
|
|
|
@ -3,7 +3,6 @@ _ = require 'underscore'
|
|||
{Actions,
|
||||
AccountStore,
|
||||
ThreadCountsStore,
|
||||
DraftCountStore,
|
||||
WorkspaceStore,
|
||||
FocusedPerspectiveStore,
|
||||
CategoryStore} = require 'nylas-exports'
|
||||
|
@ -44,8 +43,8 @@ class SidebarStore extends NylasStore
|
|||
@listenTo AccountStore, @_updateSections
|
||||
@listenTo WorkspaceStore, @_updateSections
|
||||
@listenTo ThreadCountsStore, @_updateSections
|
||||
@listenTo DraftCountStore, @_updateSections
|
||||
@listenTo CategoryStore, @_updateSections
|
||||
|
||||
@configSubscription = NylasEnv.config.observe(
|
||||
'core.workspace.showUnreadForAllCategories',
|
||||
@_updateSections
|
||||
|
|
48
internal_packages/thread-list/lib/draft-list-columns.cjsx
Normal file
48
internal_packages/thread-list/lib/draft-list-columns.cjsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
|
||||
{ListTabular, InjectedComponent} = require 'nylas-component-kit'
|
||||
{timestamp,
|
||||
subject} = require './formatting-utils'
|
||||
|
||||
snippet = (html) =>
|
||||
return "" unless html and typeof(html) is 'string'
|
||||
try
|
||||
@draftSanitizer ?= document.createElement('div')
|
||||
@draftSanitizer.innerHTML = html[0..400]
|
||||
text = @draftSanitizer.innerText
|
||||
text[0..200]
|
||||
catch
|
||||
return ""
|
||||
|
||||
c1 = new ListTabular.Column
|
||||
name: "Name"
|
||||
width: 200
|
||||
resolver: (draft) =>
|
||||
<div className="participants">
|
||||
<InjectedComponent matching={role:"Participants"}
|
||||
exposedProps={participants: [].concat(draft.to, draft.cc, draft.bcc), clickable: false}/>
|
||||
</div>
|
||||
|
||||
c2 = new ListTabular.Column
|
||||
name: "Message"
|
||||
flex: 4
|
||||
resolver: (draft) =>
|
||||
attachments = []
|
||||
if draft.files?.length > 0
|
||||
attachments = <div className="thread-icon thread-icon-attachment"></div>
|
||||
<span className="details">
|
||||
<span className="subject">{subject(draft.subject)}</span>
|
||||
<span className="snippet">{snippet(draft.body)}</span>
|
||||
{attachments}
|
||||
</span>
|
||||
|
||||
c3 = new ListTabular.Column
|
||||
name: "Date"
|
||||
flex: 1
|
||||
resolver: (draft) =>
|
||||
<span className="timestamp">{timestamp(draft.date)}</span>
|
||||
|
||||
module.exports =
|
||||
Wide: [c1, c2, c3]
|
|
@ -1,42 +1,44 @@
|
|||
NylasStore = require 'nylas-store'
|
||||
Reflux = require 'reflux'
|
||||
Rx = require 'rx-lite'
|
||||
_ = require 'underscore'
|
||||
{Message,
|
||||
Actions,
|
||||
AccountStore,
|
||||
MutableQuerySubscription,
|
||||
ObservableListDataSource,
|
||||
FocusedPerspectiveStore,
|
||||
DatabaseStore} = require 'nylas-exports'
|
||||
{ListTabular} = require 'nylas-component-kit'
|
||||
|
||||
class DraftListStore extends NylasStore
|
||||
constructor: ->
|
||||
@listenTo AccountStore, @_onAccountChanged
|
||||
@listenTo FocusedPerspectiveStore, @_onPerspectiveChanged
|
||||
@_createListDataSource()
|
||||
|
||||
@subscription = new MutableQuerySubscription(@_queryForCurrentAccount(), {asResultSet: true})
|
||||
$resultSet = Rx.Observable.fromPrivateQuerySubscription('draft-list', @subscription)
|
||||
dataSource: =>
|
||||
@_dataSource
|
||||
|
||||
@_view = new ObservableListDataSource $resultSet, ({start, end}) =>
|
||||
@subscription.replaceQuery(@_queryForCurrentAccount().page(start, end))
|
||||
# Inbound Events
|
||||
|
||||
view: =>
|
||||
@_view
|
||||
_onPerspectiveChanged: =>
|
||||
@_createListDataSource()
|
||||
|
||||
_queryForCurrentAccount: =>
|
||||
matchers = [Message.attributes.draft.equal(true)]
|
||||
account = FocusedPerspectiveStore.current().account
|
||||
# Internal
|
||||
|
||||
if account?
|
||||
matchers.push(Message.attributes.accountId.equal(account.id))
|
||||
_createListDataSource: =>
|
||||
mailboxPerspective = FocusedPerspectiveStore.current()
|
||||
|
||||
query = DatabaseStore.findAll(Message)
|
||||
.include(Message.attributes.body)
|
||||
.order(Message.attributes.date.descending())
|
||||
.where(matchers)
|
||||
.page(0, 1)
|
||||
if mailboxPerspective.drafts
|
||||
query = DatabaseStore.findAll(Message)
|
||||
.include(Message.attributes.body)
|
||||
.order(Message.attributes.date.descending())
|
||||
.where(draft: true, accountId: mailboxPerspective.accountIds)
|
||||
.page(0, 1)
|
||||
|
||||
_onAccountChanged: =>
|
||||
@subscription.replaceQuery(@_queryForCurrentAccount())
|
||||
subscription = new MutableQuerySubscription(query, {asResultSet: true})
|
||||
$resultSet = Rx.Observable.fromPrivateQuerySubscription('draft-list', subscription)
|
||||
@_dataSource = new ObservableListDataSource($resultSet, subscription.replaceRange)
|
||||
else
|
||||
@_dataSource = new ListTabular.DataSource.Empty()
|
||||
|
||||
@trigger(@)
|
||||
|
||||
module.exports = new DraftListStore()
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
{ListTabular,
|
||||
MultiselectList,
|
||||
InjectedComponent} = require 'nylas-component-kit'
|
||||
{timestamp, subject} = require './formatting-utils'
|
||||
{Actions,
|
||||
FocusedContentStore,
|
||||
DatabaseStore} = require 'nylas-exports'
|
||||
FocusedContentStore} = require 'nylas-exports'
|
||||
{ListTabular,
|
||||
FluxContainer,
|
||||
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
|
||||
|
@ -15,60 +15,26 @@ class DraftList extends React.Component
|
|||
|
||||
@containerRequired: false
|
||||
|
||||
componentWillMount: =>
|
||||
snippet = (html) =>
|
||||
return "" unless html and typeof(html) is 'string'
|
||||
try
|
||||
@draftSanitizer ?= document.createElement('div')
|
||||
@draftSanitizer.innerHTML = html[0..400]
|
||||
text = @draftSanitizer.innerText
|
||||
text[0..200]
|
||||
catch
|
||||
return ""
|
||||
|
||||
c1 = new ListTabular.Column
|
||||
name: "Name"
|
||||
width: 200
|
||||
resolver: (draft) =>
|
||||
<div className="participants">
|
||||
<InjectedComponent matching={role:"Participants"}
|
||||
exposedProps={participants: [].concat(draft.to, draft.cc, draft.bcc), clickable: false}/>
|
||||
</div>
|
||||
|
||||
c2 = new ListTabular.Column
|
||||
name: "Message"
|
||||
flex: 4
|
||||
resolver: (draft) =>
|
||||
attachments = []
|
||||
if draft.files?.length > 0
|
||||
attachments = <div className="thread-icon thread-icon-attachment"></div>
|
||||
<span className="details">
|
||||
<span className="subject">{subject(draft.subject)}</span>
|
||||
<span className="snippet">{snippet(draft.body)}</span>
|
||||
{attachments}
|
||||
</span>
|
||||
|
||||
c3 = new ListTabular.Column
|
||||
name: "Date"
|
||||
flex: 1
|
||||
resolver: (draft) =>
|
||||
<span className="timestamp">{timestamp(draft.date)}</span>
|
||||
|
||||
@columns = [c1, c2, c3]
|
||||
@commands =
|
||||
'core:remove-from-view': @_onRemoveFromView
|
||||
|
||||
render: =>
|
||||
<MultiselectList
|
||||
dataStore={DraftListStore}
|
||||
columns={@columns}
|
||||
commands={@commands}
|
||||
onDoubleClick={@_onDoubleClick}
|
||||
emptyComponent={EmptyState}
|
||||
itemPropsProvider={ -> {} }
|
||||
itemHeight={39}
|
||||
className="draft-list"
|
||||
collection="draft" />
|
||||
<FluxContainer
|
||||
stores=[DraftListStore]
|
||||
getStateFromStores={ ->
|
||||
dataSource: DraftListStore.dataSource()
|
||||
}>
|
||||
<FocusContainer collection="draft">
|
||||
<MultiselectList
|
||||
columns={DraftListColumns.Wide}
|
||||
commands={@_keymapHandlers()}
|
||||
onDoubleClick={@_onDoubleClick}
|
||||
emptyComponent={EmptyState}
|
||||
itemPropsProvider={ -> {} }
|
||||
itemHeight={39}
|
||||
className="draft-list" />
|
||||
</FocusContainer>
|
||||
</FluxContainer>
|
||||
|
||||
_keymapHandlers: ->
|
||||
'core:remove-from-view': @_onRemoveFromView
|
||||
|
||||
_onDoubleClick: (item) =>
|
||||
Actions.composePopoutDraft(item.clientId)
|
||||
|
|
|
@ -69,7 +69,6 @@ class EmptyState extends React.Component
|
|||
@displayName = 'EmptyState'
|
||||
@propTypes =
|
||||
visible: React.PropTypes.bool.isRequired
|
||||
dataSource: React.PropTypes.object
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
|
@ -85,9 +84,7 @@ class EmptyState extends React.Component
|
|||
@setState(active:true)
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
# Avoid deep comparison of dataSource, which is a very complex object
|
||||
return true if nextProps.visible isnt @props.visible
|
||||
return true if nextProps.dataSource isnt @props.dataSource
|
||||
return not _.isEqual(nextState, @state)
|
||||
|
||||
componentWillUnmount: ->
|
||||
|
|
|
@ -9,11 +9,10 @@ NylasStore = require 'nylas-store'
|
|||
FocusedContentStore,
|
||||
TaskQueueStatusStore,
|
||||
FocusedPerspectiveStore} = require 'nylas-exports'
|
||||
{ListTabular} = require 'nylas-component-kit'
|
||||
|
||||
ThreadListDataSource = require './thread-list-data-source'
|
||||
|
||||
# Public: A mutable text container with undo/redo support and the ability
|
||||
# to annotate logical regions in the text.
|
||||
class ThreadListStore extends NylasStore
|
||||
constructor: ->
|
||||
@listenTo FocusedPerspectiveStore, @_onPerspectiveChanged
|
||||
|
@ -23,18 +22,22 @@ class ThreadListStore extends NylasStore
|
|||
@_dataSource
|
||||
|
||||
createListDataSource: =>
|
||||
mailboxPerspective = FocusedPerspectiveStore.current()
|
||||
@_dataSource = new ThreadListDataSource(mailboxPerspective.threads())
|
||||
|
||||
@_dataSourceUnlisten?()
|
||||
@_dataSourceUnlisten = @_dataSource.listen(@_onDataChanged, @)
|
||||
@_dataSource = null
|
||||
|
||||
# Set up a one-time listener to focus an item in the new view
|
||||
if WorkspaceStore.layoutMode() is 'split'
|
||||
unlisten = @_dataSource.listen =>
|
||||
if @_dataSource.loaded()
|
||||
Actions.setFocus(collection: 'thread', item: @_dataSource.get(0))
|
||||
unlisten()
|
||||
threadsSubscription = FocusedPerspectiveStore.current().threads()
|
||||
if threadsSubscription
|
||||
@_dataSource = new ThreadListDataSource(threadsSubscription)
|
||||
@_dataSourceUnlisten = @_dataSource.listen(@_onDataChanged, @)
|
||||
|
||||
# Set up a one-time listener to focus an item in the new view
|
||||
if WorkspaceStore.layoutMode() is 'split'
|
||||
unlisten = @_dataSource.listen =>
|
||||
if @_dataSource.loaded()
|
||||
Actions.setFocus(collection: 'thread', item: @_dataSource.get(0))
|
||||
unlisten()
|
||||
else
|
||||
@_dataSource = new ListTabular.DataSource.Empty()
|
||||
|
||||
@trigger(@)
|
||||
Actions.setFocus(collection: 'thread', item: null)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
_ = require 'underscore'
|
||||
|
||||
Thread = require '../src/flux/models/thread'
|
||||
ListDataSource = require '../src/flux/stores/list-data-source'
|
||||
ListSelection = require '../src/flux/stores/list-selection'
|
||||
{Thread} = require 'nylas-exports'
|
||||
{ListTabular} = require 'nylas-component-kit'
|
||||
|
||||
ListDataSource = ListTabular.DataSource
|
||||
ListSelection = ListTabular.Selection
|
||||
|
||||
describe "ListSelection", ->
|
||||
beforeEach ->
|
||||
|
|
|
@ -40,7 +40,7 @@ describe "FocusedPerspectiveStore", ->
|
|||
expect(FocusedPerspectiveStore.current().categories()).toEqual([@inboxCategory])
|
||||
|
||||
describe "_onFocusPerspective", ->
|
||||
it "should focus the category and trigger when Actions.focusCategory is called", ->
|
||||
it "should focus the category and trigger", ->
|
||||
FocusedPerspectiveStore._onFocusPerspective(@userFilter)
|
||||
expect(FocusedPerspectiveStore.trigger).toHaveBeenCalled()
|
||||
expect(FocusedPerspectiveStore.current().categories()).toEqual([@userCategory])
|
||||
|
|
|
@ -2,9 +2,7 @@ _ = require 'underscore'
|
|||
EventEmitter = require('events').EventEmitter
|
||||
ListSelection = require './list-selection'
|
||||
|
||||
module.exports =
|
||||
class ListDataSource
|
||||
|
||||
constructor: ->
|
||||
@_emitter = new EventEmitter()
|
||||
@_cleanedup = false
|
||||
|
@ -17,6 +15,8 @@ class ListDataSource
|
|||
@_emitter.emit('trigger', arg)
|
||||
|
||||
listen: (callback, bindContext) ->
|
||||
unless callback instanceof Function
|
||||
throw new Error("ListDataSource: You must pass a function to `listen`")
|
||||
if @_cleanedup is true
|
||||
throw new Error("ListDataSource: You cannot listen again after removing the last listener. This is an implementation detail.")
|
||||
|
||||
|
@ -56,3 +56,17 @@ class ListDataSource
|
|||
|
||||
cleanup: ->
|
||||
@selection.cleanup()
|
||||
|
||||
class EmptyListDataSource extends ListDataSource
|
||||
loaded: -> true
|
||||
empty: -> true
|
||||
get: (idx) -> null
|
||||
getById: (id) -> null
|
||||
indexOfId: (id) -> -1
|
||||
count: -> 0
|
||||
itemsCurrentlyInViewMatching: (matchFn) -> []
|
||||
setRetainedRange: ({start, end}) ->
|
||||
|
||||
ListDataSource.Empty = EmptyListDataSource
|
||||
|
||||
module.exports = ListDataSource
|
|
@ -1,14 +1,19 @@
|
|||
Model = require '../models/model'
|
||||
DatabaseStore = require './database-store'
|
||||
_ = require 'underscore'
|
||||
|
||||
Model = require '../flux/models/model'
|
||||
DatabaseStore = require '../flux/stores/database-store'
|
||||
|
||||
module.exports =
|
||||
class ListSelection
|
||||
|
||||
constructor: (@_view, @trigger) ->
|
||||
constructor: (@_view, callback) ->
|
||||
throw new Error("new ListSelection(): You must provide a view.") unless @_view
|
||||
@_unlisten = DatabaseStore.listen(@_applyChangeRecord, @)
|
||||
@_caches = {}
|
||||
@_items = []
|
||||
@trigger = =>
|
||||
@_caches = {}
|
||||
callback()
|
||||
|
||||
cleanup: ->
|
||||
@_unlisten()
|
||||
|
@ -17,7 +22,9 @@ class ListSelection
|
|||
@_items.length
|
||||
|
||||
ids: ->
|
||||
_.pluck(@_items, 'id')
|
||||
# ListTabular asks for ids /a lot/. Cache this value and clear it on trigger.
|
||||
@_caches['ids'] ?= _.pluck(@_items, 'id')
|
||||
@_caches['ids']
|
||||
|
||||
items: -> _.clone(@_items)
|
||||
|
63
src/components/list-tabular-item.cjsx
Normal file
63
src/components/list-tabular-item.cjsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react/addons'
|
||||
{Utils} = require 'nylas-exports'
|
||||
|
||||
class ListTabularItem extends React.Component
|
||||
@displayName = 'ListTabularItem'
|
||||
@propTypes =
|
||||
metrics: React.PropTypes.object
|
||||
columns: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
|
||||
item: React.PropTypes.object.isRequired
|
||||
itemProps: React.PropTypes.object
|
||||
onSelect: React.PropTypes.func
|
||||
onClick: React.PropTypes.func
|
||||
onDoubleClick: React.PropTypes.func
|
||||
|
||||
# DO NOT DELETE unless you know what you're doing! This method cuts
|
||||
# React.Perf.wasted-time from ~300msec to 20msec by doing a deep
|
||||
# comparison of props before triggering a re-render.
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
if not Utils.isEqualReact(@props.item, nextProps.item) or @props.columns isnt nextProps.columns
|
||||
@_columnCache = null
|
||||
return true
|
||||
if not Utils.isEqualReact(_.omit(@props, 'item'), _.omit(nextProps, 'item'))
|
||||
return true
|
||||
false
|
||||
|
||||
render: =>
|
||||
className = "list-item list-tabular-item #{@props.itemProps?.className}"
|
||||
props = _.omit(@props.itemProps ? {}, 'className')
|
||||
|
||||
# It's expensive to compute the contents of columns (format timestamps, etc.)
|
||||
# We only do it if the item prop has changed.
|
||||
@_columnCache ?= @_columns()
|
||||
|
||||
<div {...props} className={className} onClick={@_onClick} style={position:'absolute', top: @props.metrics.top, width:'100%', height:@props.metrics.height, overflow: 'hidden'}>
|
||||
{@_columnCache}
|
||||
</div>
|
||||
|
||||
_columns: =>
|
||||
names = {}
|
||||
for column in (@props.columns ? [])
|
||||
if names[column.name]
|
||||
console.warn("ListTabular: Columns do not have distinct names, will cause React error! `#{column.name}` twice.")
|
||||
names[column.name] = true
|
||||
|
||||
<div key={column.name}
|
||||
displayName={column.name}
|
||||
style={_.pick(column, ['flex', 'width'])}
|
||||
className="list-column list-column-#{column.name}">
|
||||
{column.resolver(@props.item, @)}
|
||||
</div>
|
||||
|
||||
_onClick: (event) =>
|
||||
@props.onSelect?(@props.item, event)
|
||||
|
||||
@props.onClick?(@props.item, event)
|
||||
if @_lastClickTime? and Date.now() - @_lastClickTime < 350
|
||||
@props.onDoubleClick?(@props.item, event)
|
||||
|
||||
@_lastClickTime = Date.now()
|
||||
|
||||
|
||||
module.exports = ListTabularItem
|
|
@ -1,69 +1,16 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react/addons'
|
||||
ScrollRegion = require './scroll-region'
|
||||
Spinner = require './spinner'
|
||||
{Utils} = require 'nylas-exports'
|
||||
|
||||
ListDataSource = require './list-data-source'
|
||||
ListSelection = require './list-selection'
|
||||
ListTabularItem = require './list-tabular-item'
|
||||
|
||||
class ListColumn
|
||||
constructor: ({@name, @resolver, @flex, @width}) ->
|
||||
|
||||
class ListTabularItem extends React.Component
|
||||
@displayName = 'ListTabularItem'
|
||||
@propTypes =
|
||||
metrics: React.PropTypes.object
|
||||
columns: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
|
||||
item: React.PropTypes.object.isRequired
|
||||
itemProps: React.PropTypes.object
|
||||
onSelect: React.PropTypes.func
|
||||
onClick: React.PropTypes.func
|
||||
onDoubleClick: React.PropTypes.func
|
||||
|
||||
# DO NOT DELETE unless you know what you're doing! This method cuts
|
||||
# React.Perf.wasted-time from ~300msec to 20msec by doing a deep
|
||||
# comparison of props before triggering a re-render.
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
if not Utils.isEqualReact(@props.item, nextProps.item) or @props.columns isnt nextProps.columns
|
||||
@_columnCache = null
|
||||
return true
|
||||
if not Utils.isEqualReact(_.omit(@props, 'item'), _.omit(nextProps, 'item'))
|
||||
return true
|
||||
false
|
||||
|
||||
render: =>
|
||||
className = "list-item list-tabular-item #{@props.itemProps?.className}"
|
||||
props = _.omit(@props.itemProps ? {}, 'className')
|
||||
|
||||
# It's expensive to compute the contents of columns (format timestamps, etc.)
|
||||
# We only do it if the item prop has changed.
|
||||
@_columnCache ?= @_columns()
|
||||
|
||||
<div {...props} className={className} onClick={@_onClick} style={position:'absolute', top: @props.metrics.top, width:'100%', height:@props.metrics.height, overflow: 'hidden'}>
|
||||
{@_columnCache}
|
||||
</div>
|
||||
|
||||
_columns: =>
|
||||
names = {}
|
||||
for column in (@props.columns ? [])
|
||||
if names[column.name]
|
||||
console.warn("ListTabular: Columns do not have distinct names, will cause React error! `#{column.name}` twice.")
|
||||
names[column.name] = true
|
||||
|
||||
<div key={column.name}
|
||||
displayName={column.name}
|
||||
style={_.pick(column, ['flex', 'width'])}
|
||||
className="list-column list-column-#{column.name}">
|
||||
{column.resolver(@props.item, @)}
|
||||
</div>
|
||||
|
||||
_onClick: (event) =>
|
||||
@props.onSelect?(@props.item, event)
|
||||
|
||||
@props.onClick?(@props.item, event)
|
||||
if @_lastClickTime? and Date.now() - @_lastClickTime < 350
|
||||
@props.onDoubleClick?(@props.item, event)
|
||||
|
||||
@_lastClickTime = Date.now()
|
||||
|
||||
|
||||
class ListTabular extends React.Component
|
||||
@displayName = 'ListTabular'
|
||||
@propTypes =
|
||||
|
@ -79,16 +26,42 @@ class ListTabular extends React.Component
|
|||
if not @props.itemHeight
|
||||
throw new Error("ListTabular: You must provide an itemHeight - raising to avoid divide by zero errors.")
|
||||
|
||||
@state =
|
||||
renderedRangeStart: -1
|
||||
renderedRangeEnd: -1
|
||||
@state = @buildStateForRange(start: -1, end: -1)
|
||||
|
||||
componentDidMount: =>
|
||||
window.addEventListener('resize', @onWindowResize, true)
|
||||
@setupDataSource(@props.dataSource)
|
||||
@updateRangeState()
|
||||
|
||||
componentWillUnmount: =>
|
||||
window.removeEventListener('resize', @onWindowResize, true)
|
||||
@_unlisten?()
|
||||
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
if nextProps.dataSource isnt @props.dataSource
|
||||
@setupDataSource(nextProps.dataSource)
|
||||
@setState(@buildStateForRange(dataSource: nextProps.dataSource))
|
||||
|
||||
setupDataSource: (dataSource) =>
|
||||
@_unlisten?()
|
||||
@_unlisten = dataSource.listen =>
|
||||
@setState(@buildStateForRange())
|
||||
|
||||
buildStateForRange: ({dataSource, start, end} = {}) =>
|
||||
start ?= @state.renderedRangeStart
|
||||
end ?= @state.renderedRangeEnd
|
||||
dataSource ?= @props.dataSource
|
||||
|
||||
items = {}
|
||||
[start..end].forEach (idx) =>
|
||||
items[idx] = dataSource.get(idx)
|
||||
|
||||
renderedRangeStart: start
|
||||
renderedRangeEnd: end
|
||||
count: dataSource.count()
|
||||
loaded: dataSource.loaded()
|
||||
empty: dataSource.empty()
|
||||
items: items
|
||||
|
||||
componentDidUpdate: (prevProps, prevState) =>
|
||||
# If our view has been swapped out for an entirely different one,
|
||||
|
@ -120,7 +93,7 @@ class ListTabular extends React.Component
|
|||
# Expand the start/end so that you can advance the keyboard cursor fast and
|
||||
# we have items to move to and then scroll to.
|
||||
rangeStart = Math.max(0, rangeStart - 2)
|
||||
rangeEnd = Math.min(rangeEnd + 2, @props.dataSource.count() + 1)
|
||||
rangeEnd = Math.min(rangeEnd + 2, @state.count + 1)
|
||||
|
||||
# Final sanity check to prevent needless work
|
||||
return if rangeStart is @state.renderedRangeStart and
|
||||
|
@ -132,46 +105,45 @@ class ListTabular extends React.Component
|
|||
start: rangeStart
|
||||
end: rangeEnd
|
||||
|
||||
@setState
|
||||
renderedRangeStart: rangeStart
|
||||
renderedRangeEnd: rangeEnd
|
||||
@setState(@buildStateForRange(start: rangeStart, end: rangeEnd))
|
||||
|
||||
render: =>
|
||||
innerStyles =
|
||||
height: @props.dataSource.count() * @props.itemHeight
|
||||
height: @state.count * @props.itemHeight
|
||||
|
||||
<ScrollRegion
|
||||
ref="container"
|
||||
onScroll={@onScroll}
|
||||
tabIndex="-1"
|
||||
className="list-container list-tabular"
|
||||
scrollTooltipComponent={@props.scrollTooltipComponent}>
|
||||
<div className="list-rows" style={innerStyles}>
|
||||
{@_rows()}
|
||||
</div>
|
||||
</ScrollRegion>
|
||||
emptyElement = false
|
||||
if @props.emptyComponent
|
||||
emptyElement = <@props.emptyComponent visible={@state.loaded and @state.empty} />
|
||||
|
||||
<div className={@props.className}>
|
||||
<ScrollRegion
|
||||
ref="container"
|
||||
onScroll={@onScroll}
|
||||
tabIndex="-1"
|
||||
className="list-container list-tabular"
|
||||
scrollTooltipComponent={@props.scrollTooltipComponent}>
|
||||
<div className="list-rows" style={innerStyles}>
|
||||
{@_rows()}
|
||||
</div>
|
||||
</ScrollRegion>
|
||||
<Spinner visible={!@state.loaded and @state.empty} />
|
||||
{emptyElement}
|
||||
</div>
|
||||
|
||||
_rows: =>
|
||||
rows = []
|
||||
[@state.renderedRangeStart..@state.renderedRangeEnd-1].map (idx) =>
|
||||
item = @state.items[idx]
|
||||
return false unless item
|
||||
|
||||
for idx in [@state.renderedRangeStart..@state.renderedRangeEnd-1]
|
||||
item = @props.dataSource.get(idx)
|
||||
continue unless item
|
||||
|
||||
itemProps = {}
|
||||
if @props.itemPropsProvider
|
||||
itemProps = @props.itemPropsProvider(item)
|
||||
|
||||
rows.push <ListTabularItem key={item.id ? idx}
|
||||
item={item}
|
||||
itemProps={itemProps}
|
||||
metrics={top: idx * @props.itemHeight, height: @props.itemHeight}
|
||||
columns={@props.columns}
|
||||
onSelect={@props.onSelect}
|
||||
onClick={@props.onClick}
|
||||
onReorder={@props.onReorder}
|
||||
onDoubleClick={@props.onDoubleClick} />
|
||||
rows
|
||||
<ListTabularItem key={item.id ? idx}
|
||||
item={item}
|
||||
itemProps={@props.itemPropsProvider?(item) ? {}}
|
||||
metrics={top: idx * @props.itemHeight, height: @props.itemHeight}
|
||||
columns={@props.columns}
|
||||
onSelect={@props.onSelect}
|
||||
onClick={@props.onClick}
|
||||
onReorder={@props.onReorder}
|
||||
onDoubleClick={@props.onDoubleClick} />
|
||||
|
||||
# Public: Scroll to the DOM node provided.
|
||||
#
|
||||
|
@ -185,5 +157,7 @@ class ListTabular extends React.Component
|
|||
|
||||
ListTabular.Item = ListTabularItem
|
||||
ListTabular.Column = ListColumn
|
||||
ListTabular.Selection = ListSelection
|
||||
ListTabular.DataSource = ListDataSource
|
||||
|
||||
module.exports = ListTabular
|
||||
|
|
|
@ -32,11 +32,7 @@ class MultiselectList extends React.Component
|
|||
className: React.PropTypes.string.isRequired
|
||||
columns: React.PropTypes.array.isRequired
|
||||
itemPropsProvider: React.PropTypes.func.isRequired
|
||||
itemHeight: React.PropTypes.number.isRequired
|
||||
scrollTooltipComponent: React.PropTypes.func
|
||||
emptyComponent: React.PropTypes.func
|
||||
keymapHandlers: React.PropTypes.object
|
||||
onDoubleClick: React.PropTypes.func
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
@ -103,33 +99,21 @@ class MultiselectList extends React.Component
|
|||
props = @props.itemPropsProvider(item)
|
||||
props.className ?= ''
|
||||
props.className += " " + classNames
|
||||
'selected': item.id in @state.selectedIds
|
||||
'selected': item.id in @props.dataSource.selection.ids()
|
||||
'focused': @state.handler.shouldShowFocus() and item.id is @props.focusedId
|
||||
'keyboard-cursor': @state.handler.shouldShowKeyboardCursor() and item.id is @props.keyboardCursorId
|
||||
props['data-item-id'] = item.id
|
||||
props
|
||||
|
||||
emptyElement = []
|
||||
if @props.emptyComponent
|
||||
emptyElement = <@props.emptyComponent
|
||||
visible={@state.loaded and @state.empty}
|
||||
dataSource={@props.dataSource} />
|
||||
|
||||
<KeyCommandsRegion globalHandlers={@_globalKeymapHandlers()} className="multiselect-list">
|
||||
<div className={className} {...otherProps}>
|
||||
<ListTabular
|
||||
ref="list"
|
||||
columns={@state.computedColumns}
|
||||
scrollTooltipComponent={@props.scrollTooltipComponent}
|
||||
dataSource={@props.dataSource}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={@props.itemHeight}
|
||||
onSelect={@_onClickItem}
|
||||
onDoubleClick={@props.onDoubleClick} />
|
||||
|
||||
<Spinner visible={!@state.loaded and @state.empty} />
|
||||
{emptyElement}
|
||||
</div>
|
||||
<ListTabular
|
||||
ref="list"
|
||||
className={className}
|
||||
columns={@state.computedColumns}
|
||||
dataSource={@props.dataSource}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
onSelect={@_onClickItem}
|
||||
{...otherProps} />
|
||||
</KeyCommandsRegion>
|
||||
else
|
||||
<div className={className} {...otherProps}>
|
||||
|
@ -216,9 +200,6 @@ class MultiselectList extends React.Component
|
|||
columns: props.columns
|
||||
computedColumns: computedColumns
|
||||
layoutMode: layoutMode
|
||||
selectedIds: props.dataSource.selection.ids()
|
||||
loaded: props.dataSource.loaded()
|
||||
empty: props.dataSource.empty()
|
||||
|
||||
# Public Methods
|
||||
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
Rx = require 'rx-lite'
|
||||
NylasStore = require 'nylas-store'
|
||||
Actions = require '../actions'
|
||||
Message = require '../models/message'
|
||||
Account = require '../models/account'
|
||||
DatabaseStore = require './database-store'
|
||||
AccountStore = require './account-store'
|
||||
FocusedPerspectiveStore = require './focused-perspective-store'
|
||||
|
||||
###
|
||||
Public: The DraftCountStore exposes a simple API for getting the number of
|
||||
drafts in the user's account. If you plugin needs the number of drafts,
|
||||
it's more efficient to observe the DraftCountStore than retrieve the value
|
||||
yourself from the database.
|
||||
|
||||
The DraftCountStore is only available in the main window.
|
||||
###
|
||||
|
||||
if not NylasEnv.isMainWindow() and not NylasEnv.inSpecMode() then return
|
||||
|
||||
class DraftCountStore extends NylasStore
|
||||
|
||||
constructor: ->
|
||||
@_counts = {}
|
||||
@_total = 0
|
||||
@_disposable = Rx.Observable.fromQuery(
|
||||
DatabaseStore.findAll(Message).where([Message.attributes.draft.equal(true)])
|
||||
).subscribe(@_onDraftsChanged)
|
||||
|
||||
totalCount: ->
|
||||
@_total
|
||||
|
||||
# Public: Returns the number of drafts for the given account
|
||||
count: (accountOrId)->
|
||||
return 0 unless accountOrId
|
||||
accountId = if accountOrId instanceof Account
|
||||
accountOrId.id
|
||||
else
|
||||
accountOrId
|
||||
@_counts[accountId]
|
||||
|
||||
_onDraftsChanged: (drafts) =>
|
||||
@_total = 0
|
||||
@_counts = {}
|
||||
for account in AccountStore.accounts()
|
||||
@_counts[account.id] = _.where(drafts, accountId: account.id).length
|
||||
@_total += @_counts[account.id]
|
||||
@trigger()
|
||||
|
||||
|
||||
module.exports = new DraftCountStore()
|
|
@ -33,8 +33,6 @@ class FocusedPerspectiveStore extends NylasStore
|
|||
|
||||
_onFocusPerspective: (perspective) =>
|
||||
return if perspective.isEqual(@_current)
|
||||
if WorkspaceStore.Sheet.Threads
|
||||
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
|
||||
@_setPerspective(perspective)
|
||||
|
||||
_onFocusAccounts: (accountsOrIds) =>
|
||||
|
|
|
@ -5,7 +5,8 @@ Message = require '../models/message'
|
|||
QuerySubscriptionPool = require '../models/query-subscription-pool'
|
||||
QuerySubscription = require '../models/query-subscription'
|
||||
MutableQuerySubscription = require '../models/mutable-query-subscription'
|
||||
ListDataSource = require './list-data-source'
|
||||
|
||||
{ListTabular} = require 'nylas-component-kit'
|
||||
|
||||
###
|
||||
This class takes an observable which vends QueryResultSets and adapts it so that
|
||||
|
@ -14,7 +15,7 @@ you can make it the data source of a MultiselectList.
|
|||
When the MultiselectList is refactored to take an Observable, this class should
|
||||
go away!
|
||||
###
|
||||
class ObservableListDataSource extends ListDataSource
|
||||
class ObservableListDataSource extends ListTabular.DataSource
|
||||
|
||||
constructor: ($resultSetObservable, @_setRetainedRange) ->
|
||||
super
|
||||
|
|
|
@ -118,7 +118,6 @@ class NylasExports
|
|||
@require "ContactStore", 'flux/stores/contact-store'
|
||||
@require "CategoryStore", 'flux/stores/category-store'
|
||||
@require "WorkspaceStore", 'flux/stores/workspace-store'
|
||||
@require "DraftCountStore", 'flux/stores/draft-count-store'
|
||||
@require "FileUploadStore", 'flux/stores/file-upload-store'
|
||||
@require "MailRulesStore", 'flux/stores/mail-rules-store'
|
||||
@require "ThreadCountsStore", 'flux/stores/thread-counts-store'
|
||||
|
|
|
@ -18,6 +18,9 @@ class MailboxPerspective
|
|||
@forNothing: ->
|
||||
new EmptyMailboxPerspective()
|
||||
|
||||
@forDrafts: (accountsOrIds) ->
|
||||
new DraftsMailboxPerspective(accountsOrIds)
|
||||
|
||||
@forCategory: (category) ->
|
||||
return @forNothing() unless category
|
||||
new CategoryMailboxPerspective([category])
|
||||
|
@ -111,6 +114,20 @@ class SearchMailboxPerspective extends MailboxPerspective
|
|||
false
|
||||
|
||||
|
||||
class DraftsMailboxPerspective extends MailboxPerspective
|
||||
constructor: (@accountIds) ->
|
||||
super(@accountIds)
|
||||
@name = "Drafts"
|
||||
@iconName = "drafts.png"
|
||||
@drafts = true # The DraftListStore looks for this
|
||||
@
|
||||
|
||||
threads: =>
|
||||
null
|
||||
|
||||
canApplyToThreads: =>
|
||||
false
|
||||
|
||||
class StarredMailboxPerspective extends MailboxPerspective
|
||||
constructor: (@accountIds) ->
|
||||
super(@accountIds)
|
||||
|
|
Loading…
Reference in a new issue