diff --git a/internal_packages/account-sidebar/lib/sidebar-item.coffee b/internal_packages/account-sidebar/lib/sidebar-item.coffee index 3a191e94b..e738d204a 100644 --- a/internal_packages/account-sidebar/lib/sidebar-item.coffee +++ b/internal_packages/account-sidebar/lib/sidebar-item.coffee @@ -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 diff --git a/internal_packages/account-sidebar/lib/sidebar-section.coffee b/internal_packages/account-sidebar/lib/sidebar-section.coffee index 1f05b0bb9..e200b6e0f 100644 --- a/internal_packages/account-sidebar/lib/sidebar-section.coffee +++ b/internal_packages/account-sidebar/lib/sidebar-section.coffee @@ -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 diff --git a/internal_packages/account-sidebar/lib/sidebar-store.coffee b/internal_packages/account-sidebar/lib/sidebar-store.coffee index 16a8e36d8..e5052c553 100644 --- a/internal_packages/account-sidebar/lib/sidebar-store.coffee +++ b/internal_packages/account-sidebar/lib/sidebar-store.coffee @@ -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 diff --git a/internal_packages/thread-list/lib/draft-list-columns.cjsx b/internal_packages/thread-list/lib/draft-list-columns.cjsx new file mode 100644 index 000000000..6725f8c5b --- /dev/null +++ b/internal_packages/thread-list/lib/draft-list-columns.cjsx @@ -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) => +
+ +
+ +c2 = new ListTabular.Column + name: "Message" + flex: 4 + resolver: (draft) => + attachments = [] + if draft.files?.length > 0 + attachments =
+ + {subject(draft.subject)} + {snippet(draft.body)} + {attachments} + + +c3 = new ListTabular.Column + name: "Date" + flex: 1 + resolver: (draft) => + {timestamp(draft.date)} + +module.exports = + Wide: [c1, c2, c3] diff --git a/internal_packages/thread-list/lib/draft-list-store.coffee b/internal_packages/thread-list/lib/draft-list-store.coffee index 277325874..1e0c04f29 100644 --- a/internal_packages/thread-list/lib/draft-list-store.coffee +++ b/internal_packages/thread-list/lib/draft-list-store.coffee @@ -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() diff --git a/internal_packages/thread-list/lib/draft-list.cjsx b/internal_packages/thread-list/lib/draft-list.cjsx index 3ca1af021..c600df2f0 100644 --- a/internal_packages/thread-list/lib/draft-list.cjsx +++ b/internal_packages/thread-list/lib/draft-list.cjsx @@ -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) => -
- -
- - c2 = new ListTabular.Column - name: "Message" - flex: 4 - resolver: (draft) => - attachments = [] - if draft.files?.length > 0 - attachments =
- - {subject(draft.subject)} - {snippet(draft.body)} - {attachments} - - - c3 = new ListTabular.Column - name: "Date" - flex: 1 - resolver: (draft) => - {timestamp(draft.date)} - - @columns = [c1, c2, c3] - @commands = - 'core:remove-from-view': @_onRemoveFromView - render: => - {} } - itemHeight={39} - className="draft-list" - collection="draft" /> + + dataSource: DraftListStore.dataSource() + }> + + {} } + itemHeight={39} + className="draft-list" /> + + + + _keymapHandlers: -> + 'core:remove-from-view': @_onRemoveFromView _onDoubleClick: (item) => Actions.composePopoutDraft(item.clientId) diff --git a/internal_packages/thread-list/lib/empty-state.cjsx b/internal_packages/thread-list/lib/empty-state.cjsx index b65cd3687..08f89c5b0 100644 --- a/internal_packages/thread-list/lib/empty-state.cjsx +++ b/internal_packages/thread-list/lib/empty-state.cjsx @@ -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: -> diff --git a/internal_packages/thread-list/lib/thread-list-store.coffee b/internal_packages/thread-list/lib/thread-list-store.coffee index ddfc1788f..9f0f50c75 100644 --- a/internal_packages/thread-list/lib/thread-list-store.coffee +++ b/internal_packages/thread-list/lib/thread-list-store.coffee @@ -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) diff --git a/spec/list-selection-spec.coffee b/spec/list-selection-spec.coffee index bb959a96c..628e4f377 100644 --- a/spec/list-selection-spec.coffee +++ b/spec/list-selection-spec.coffee @@ -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 -> diff --git a/spec/stores/focused-perspective-store-spec.coffee b/spec/stores/focused-perspective-store-spec.coffee index b8bececc3..fee30ecad 100644 --- a/spec/stores/focused-perspective-store-spec.coffee +++ b/spec/stores/focused-perspective-store-spec.coffee @@ -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]) diff --git a/src/flux/stores/list-data-source.coffee b/src/components/list-data-source.coffee similarity index 79% rename from src/flux/stores/list-data-source.coffee rename to src/components/list-data-source.coffee index 44d33614a..b12a0c34d 100644 --- a/src/flux/stores/list-data-source.coffee +++ b/src/components/list-data-source.coffee @@ -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 diff --git a/src/flux/stores/list-selection.coffee b/src/components/list-selection.coffee similarity index 92% rename from src/flux/stores/list-selection.coffee rename to src/components/list-selection.coffee index d00049f70..811242ff1 100644 --- a/src/flux/stores/list-selection.coffee +++ b/src/components/list-selection.coffee @@ -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) diff --git a/src/components/list-tabular-item.cjsx b/src/components/list-tabular-item.cjsx new file mode 100644 index 000000000..0674091eb --- /dev/null +++ b/src/components/list-tabular-item.cjsx @@ -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() + +
+ {@_columnCache} +
+ + _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 + +
+ {column.resolver(@props.item, @)} +
+ + _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 diff --git a/src/components/list-tabular.cjsx b/src/components/list-tabular.cjsx index 76c23423e..d0d4969fe 100644 --- a/src/components/list-tabular.cjsx +++ b/src/components/list-tabular.cjsx @@ -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() - -
- {@_columnCache} -
- - _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 - -
- {column.resolver(@props.item, @)} -
- - _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 - -
- {@_rows()} -
-
+ emptyElement = false + if @props.emptyComponent + emptyElement = <@props.emptyComponent visible={@state.loaded and @state.empty} /> + +
+ +
+ {@_rows()} +
+
+ + {emptyElement} +
_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 - rows + # 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 diff --git a/src/components/multiselect-list.cjsx b/src/components/multiselect-list.cjsx index 3083ccb76..f93cfedd7 100644 --- a/src/components/multiselect-list.cjsx +++ b/src/components/multiselect-list.cjsx @@ -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} /> - -
- - - - {emptyElement} -
+
else
@@ -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 diff --git a/src/flux/stores/draft-count-store.coffee b/src/flux/stores/draft-count-store.coffee deleted file mode 100644 index bda74a007..000000000 --- a/src/flux/stores/draft-count-store.coffee +++ /dev/null @@ -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() diff --git a/src/flux/stores/focused-perspective-store.coffee b/src/flux/stores/focused-perspective-store.coffee index 0f91cbd34..21c008631 100644 --- a/src/flux/stores/focused-perspective-store.coffee +++ b/src/flux/stores/focused-perspective-store.coffee @@ -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) => diff --git a/src/flux/stores/observable-list-data-source.coffee b/src/flux/stores/observable-list-data-source.coffee index 6fb32988a..c52261ff8 100644 --- a/src/flux/stores/observable-list-data-source.coffee +++ b/src/flux/stores/observable-list-data-source.coffee @@ -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 diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee index 37937503d..2e2aab7d8 100644 --- a/src/global/nylas-exports.coffee +++ b/src/global/nylas-exports.coffee @@ -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' diff --git a/src/mailbox-perspective.coffee b/src/mailbox-perspective.coffee index b0c395840..275b7dcdf 100644 --- a/src/mailbox-perspective.coffee +++ b/src/mailbox-perspective.coffee @@ -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)