From 343e592569771b7ffe89f879d01147914bfb5d99 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Mon, 9 Mar 2015 18:25:53 -0700 Subject: [PATCH] feat(draft-list) Refactor thread-list, create draft-list Summary: Adds the draft list using a refactored list-tabular class. Also fixes several draft bugs that appeared after allowing editing. Test Plan: Run tests (need to test new ListTabular component ASAP) Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1272 --- exports/inbox-exports.coffee | 1 + exports/ui-components.coffee | 1 + .../lib/account-sidebar-store.coffee | 6 +- .../lib/account-sidebar-tag-item.cjsx | 10 +- .../account-sidebar/lib/main.cjsx | 2 +- .../composer/lib/composer-view.cjsx | 9 +- internal_packages/composer/lib/main.cjsx | 2 +- internal_packages/message-list/lib/main.cjsx | 2 +- internal_packages/notifications/lib/main.cjsx | 4 +- internal_packages/search-bar/lib/main.cjsx | 2 +- .../thread-list/lib/draft-list.cjsx | 99 ++++++ .../thread-list/lib/formatting-utils.cjsx | 20 ++ internal_packages/thread-list/lib/main.cjsx | 35 ++- .../thread-list/lib/thread-list-column.coffee | 3 - .../lib/thread-list-item-mixin.cjsx | 42 --- .../thread-list/lib/thread-list-mixin.cjsx | 66 ---- .../lib/thread-list-tabular-item.cjsx | 36 --- .../thread-list/lib/thread-list-tabular.cjsx | 136 -------- .../thread-list/lib/thread-list.cjsx | 138 +++++++++ .../thread-list/spec/thread-list-spec.cjsx | 290 ++++++++---------- .../thread-list/stylesheets/thread-list.less | 122 ++------ keymaps/base.cson | 18 +- spec-inbox/models/model-spec.coffee | 14 - src/atom.coffee | 2 +- src/browser/edgehill-application.coffee | 3 +- .../components/list-narrow-item.cjsx | 0 .../components/list-narrow.cjsx | 0 src/components/list-tabular.cjsx | 99 ++++++ src/flux/action-bridge.coffee | 18 +- src/flux/actions.coffee | 1 + src/flux/models/model.coffee | 3 - src/flux/stores/draft-store-proxy.coffee | 5 +- src/flux/stores/draft-store.coffee | 42 ++- src/flux/stores/workspace-store.coffee | 24 ++ src/sheet-store.cjsx | 2 +- src/sheet.cjsx | 4 +- static/components/list-tabular.less | 74 +++++ static/index.less | 1 + 38 files changed, 719 insertions(+), 617 deletions(-) create mode 100644 internal_packages/thread-list/lib/draft-list.cjsx create mode 100644 internal_packages/thread-list/lib/formatting-utils.cjsx delete mode 100644 internal_packages/thread-list/lib/thread-list-column.coffee delete mode 100644 internal_packages/thread-list/lib/thread-list-item-mixin.cjsx delete mode 100644 internal_packages/thread-list/lib/thread-list-mixin.cjsx delete mode 100644 internal_packages/thread-list/lib/thread-list-tabular-item.cjsx delete mode 100644 internal_packages/thread-list/lib/thread-list-tabular.cjsx create mode 100644 internal_packages/thread-list/lib/thread-list.cjsx rename internal_packages/thread-list/lib/thread-list-narrow-item.cjsx => src/components/list-narrow-item.cjsx (100%) rename internal_packages/thread-list/lib/thread-list-narrow.cjsx => src/components/list-narrow.cjsx (100%) create mode 100644 src/components/list-tabular.cjsx create mode 100644 src/flux/stores/workspace-store.coffee create mode 100644 static/components/list-tabular.less diff --git a/exports/inbox-exports.coffee b/exports/inbox-exports.coffee index 453795759..6b5d4d96e 100644 --- a/exports/inbox-exports.coffee +++ b/exports/inbox-exports.coffee @@ -44,6 +44,7 @@ module.exports = NamespaceStore: require '../src/flux/stores/namespace-store' FileUploadStore: require '../src/flux/stores/file-upload-store' FileDownloadStore: require '../src/flux/stores/file-download-store' + WorkspaceStore: require '../src/flux/stores/workspace-store' ## TODO move to inside of individual Salesforce package. See https://trello.com/c/tLAGLyeb/246-move-salesforce-models-into-individual-package-db-models-for-packages-various-refactors SalesforceAssociation: require '../src/flux/models/salesforce-association' diff --git a/exports/ui-components.coffee b/exports/ui-components.coffee index 3bbfebd98..dcdb01536 100644 --- a/exports/ui-components.coffee +++ b/exports/ui-components.coffee @@ -8,3 +8,4 @@ module.exports = ResizableRegion: require '../src/components/resizable-region' Flexbox: require '../src/components/flexbox' RetinaImg: require '../src/components/retina-img' + ListTabular: require '../src/components/list-tabular' diff --git a/internal_packages/account-sidebar/lib/account-sidebar-store.coffee b/internal_packages/account-sidebar/lib/account-sidebar-store.coffee index cdd88d868..1a66647e6 100644 --- a/internal_packages/account-sidebar/lib/account-sidebar-store.coffee +++ b/internal_packages/account-sidebar/lib/account-sidebar-store.coffee @@ -24,7 +24,7 @@ AccountSidebarStore = Reflux.createStore @_selectedId = null _registerListeners: -> - @listenTo Actions.selectTagId, @_onSelectTagID + @listenTo Actions.selectTagId, @_onSelectTagId @listenTo DatabaseStore, @_onDataChanged @listenTo NamespaceStore, @_onNamespaceChanged @@ -93,8 +93,8 @@ AccountSidebarStore = Reflux.createStore if change.objectClass == Thread.name @_populateUnreadCountsDebounced() - _onSelectTagID: (tagID) -> - @_selectedId = tagID + _onSelectTagId: (tagId) -> + @_selectedId = tagId @trigger(@) module.exports = AccountSidebarStore diff --git a/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx b/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx index 032a166a6..4705782f3 100644 --- a/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx +++ b/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx @@ -5,7 +5,10 @@ React = require 'react' module.exports = AccountSidebarTagItem = React.createClass render: -> - unread = if @props.tag.unreadCount > 0 then
{@props.tag.unreadCount}
else [] + unread = [] + if @props.tag.unreadCount > 0 + unread =
{@props.tag.unreadCount}
+ classSet = React.addons.classSet 'item': true 'item-tag': true @@ -19,4 +22,9 @@ AccountSidebarTagItem = React.createClass _onClick: (event) -> event.preventDefault() + + if @props.tag.id is 'drafts' + Actions.selectView('drafts') + else + Actions.selectView('threads') Actions.selectTagId(@props.tag.id) diff --git a/internal_packages/account-sidebar/lib/main.cjsx b/internal_packages/account-sidebar/lib/main.cjsx index 5d8f60960..69aa3d95c 100644 --- a/internal_packages/account-sidebar/lib/main.cjsx +++ b/internal_packages/account-sidebar/lib/main.cjsx @@ -9,4 +9,4 @@ module.exports = ComponentRegistry.register view: AccountSidebar name: 'AccountSidebar' - role: 'ThreadList:Left' + role: 'Root:Left' diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx index 2664d2eba..e247f5f61 100644 --- a/internal_packages/composer/lib/composer-view.cjsx +++ b/internal_packages/composer/lib/composer-view.cjsx @@ -116,7 +116,9 @@ ComposerView = React.createClass
- {@_trashBtn()} + + @@ -206,11 +208,6 @@ ComposerView = React.createClass focus: (field) -> @refs[field]?.focus?() if @isMounted() - _trashBtn: -> - if @props.mode isnt "fullwindow" - - _footerComponents: -> (@state.FooterComponents ? []).map (Component) => idGen += 1 diff --git a/internal_packages/composer/lib/main.cjsx b/internal_packages/composer/lib/main.cjsx index b72b6482e..205af132d 100644 --- a/internal_packages/composer/lib/main.cjsx +++ b/internal_packages/composer/lib/main.cjsx @@ -76,4 +76,4 @@ module.exports = ComponentRegistry.register view: NewComposeButton name: 'NewComposeButton' - role: 'ThreadList:Left:Toolbar' + role: 'Root:Left:Toolbar' diff --git a/internal_packages/message-list/lib/main.cjsx b/internal_packages/message-list/lib/main.cjsx index 3a4c9a65a..1380c78a8 100644 --- a/internal_packages/message-list/lib/main.cjsx +++ b/internal_packages/message-list/lib/main.cjsx @@ -15,7 +15,7 @@ module.exports = ComponentRegistry.register name: 'MessageList' - role: 'ThreadList:Right' + role: 'Root:Right' view: MessageList deactivate: -> diff --git a/internal_packages/notifications/lib/main.cjsx b/internal_packages/notifications/lib/main.cjsx index e6dee488a..898de4534 100644 --- a/internal_packages/notifications/lib/main.cjsx +++ b/internal_packages/notifications/lib/main.cjsx @@ -10,12 +10,12 @@ module.exports = ComponentRegistry.register view: Notifications name: 'Notifications' - role: 'ThreadList:Center' + role: 'Root:Center' ComponentRegistry.register view: NotificationsStickyBar name: 'NotificationsStickyBar' - role: 'ThreadList:Top' + role: 'Root:Top' deactivate: -> ComponentRegistry.unregister('NotificationsStickyBar') diff --git a/internal_packages/search-bar/lib/main.cjsx b/internal_packages/search-bar/lib/main.cjsx index 6a75b6135..bd0eb57e3 100644 --- a/internal_packages/search-bar/lib/main.cjsx +++ b/internal_packages/search-bar/lib/main.cjsx @@ -13,7 +13,7 @@ module.exports = ComponentRegistry.register view: SearchBar name: 'SearchBar' - role: 'ThreadList:Right:Toolbar' + role: 'Root:Right:Toolbar' deactivate: -> ComponentRegistry.unregister 'SearchBar' diff --git a/internal_packages/thread-list/lib/draft-list.cjsx b/internal_packages/thread-list/lib/draft-list.cjsx new file mode 100644 index 000000000..b10ee7db7 --- /dev/null +++ b/internal_packages/thread-list/lib/draft-list.cjsx @@ -0,0 +1,99 @@ +_ = require 'underscore-plus' +moment = require "moment" +React = require 'react' +{ListTabular} = require 'ui-components' +{timestamp, subject} = require './formatting-utils' +{Actions, + DraftStore, + ComponentRegistry, + DatabaseStore} = require 'inbox-exports' + +module.exports = +DraftList = React.createClass + displayName: 'DraftList' + + mixins: [ComponentRegistry.Mixin] + components: ['Participants'] + + getInitialState: -> + items: DraftStore.items() + columns: @_computeColumns() + selectedId: null + + componentDidMount: -> + @draft_store_unsubscribe = DraftStore.listen @_onChange + @body_unsubscriber = atom.commands.add 'body', + 'application:previous-item': => @_onShiftSelectedIndex(-1) + 'application:next-item': => @_onShiftSelectedIndex(1) + 'application:remove-item': @_onDeleteSelected + + componentWillUnmount: -> + @draft_store_unsubscribe() + @body_unsubscriber.dispose() + + render: -> +
+ +
+ + _onSelect: (item) -> + @setState + selectedId: item.id + + _onDoubleClick: (item) -> + DatabaseStore.localIdForModel(item).then (localId) -> + Actions.composePopoutDraft(localId) + + _computeColumns: -> + c1 = new ListTabular.Column + name: "Name" + flex: 2 + resolver: (draft) => + Participants = @state.Participants +
+ +
+ + c2 = new ListTabular.Column + name: "Subject" + flex: 3 + resolver: (draft) -> + {subject(draft.subject)} + + c3 = new ListTabular.Column + name: "Snippet" + flex: 4 + resolver: (draft) -> + {draft.body} + + c4 = new ListTabular.Column + name: "Date" + flex: 1 + resolver: (draft) -> + {timestamp(draft.date)} + + [c1, c2, c3, c4] + + _onShiftSelectedIndex: (delta) -> + item = _.find @state.items, (draft) => draft.id is @state.selectedId + index = if item then @state.items.indexOf(item) else -1 + index = Math.max(0, Math.min(index + delta, @state.items.length-1)) + @setState + selectedId: @state.items[index].id + + _onDeleteSelected: -> + item = _.find @state.items, (draft) => draft.id is @state.selectedId + + DatabaseStore.localIdForModel(item).then (localId) -> + Actions.destroyDraft(localId) + @_onShiftSelectedIndex(-1) + + _onChange: -> + @setState + items: DraftStore.items() diff --git a/internal_packages/thread-list/lib/formatting-utils.cjsx b/internal_packages/thread-list/lib/formatting-utils.cjsx new file mode 100644 index 000000000..4256c7e3c --- /dev/null +++ b/internal_packages/thread-list/lib/formatting-utils.cjsx @@ -0,0 +1,20 @@ +moment = require "moment" +React = require 'react' + +module.exports = + timestamp: (time) -> + diff = moment().diff(time, 'days', true) + if diff <= 1 + format = "h:mm a" + else if diff > 1 and diff <= 365 + format = "MMM D" + else + format = "MMM D YYYY" + moment(time).format(format) + + subject: (subj) -> + if (subj ? "").trim().length is 0 + return (No Subject) + else + return subj + diff --git a/internal_packages/thread-list/lib/main.cjsx b/internal_packages/thread-list/lib/main.cjsx index 0dfc5aabd..f113facf7 100644 --- a/internal_packages/thread-list/lib/main.cjsx +++ b/internal_packages/thread-list/lib/main.cjsx @@ -1,11 +1,36 @@ _ = require 'underscore-plus' React = require "react" -{ComponentRegistry} = require "inbox-exports" -ThreadListTabular = require "./thread-list-tabular" +{ComponentRegistry, WorkspaceStore} = require "inbox-exports" +ThreadList = require "./thread-list" +DraftList = require "./draft-list" + +RootCenterComponent = React.createClass + displayName: 'RootCenterComponent' + + getInitialState: -> + view: WorkspaceStore.selectedView() + + componentDidMount: -> + @unsubscribe = WorkspaceStore.listen @_onStoreChange + + componentWillUnmount: -> + @unsubscribe() if @unsubscribe + + render: -> + views = + 'threads': ThreadList + 'drafts': DraftList + view = views[@state.view] + + + _onStoreChange: -> + @setState + view: WorkspaceStore.selectedView() + module.exports = activate: (@state={}) -> ComponentRegistry.register - view: ThreadListTabular - name: 'ThreadListTabular' - role: 'ThreadList:Center' + view: RootCenterComponent + name: 'RootCenterComponent' + role: 'Root:Center' \ No newline at end of file diff --git a/internal_packages/thread-list/lib/thread-list-column.coffee b/internal_packages/thread-list/lib/thread-list-column.coffee deleted file mode 100644 index 01780d5c2..000000000 --- a/internal_packages/thread-list/lib/thread-list-column.coffee +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = -class ThreadListColumn - constructor: ({@name, @resolver, @flex}) -> diff --git a/internal_packages/thread-list/lib/thread-list-item-mixin.cjsx b/internal_packages/thread-list/lib/thread-list-item-mixin.cjsx deleted file mode 100644 index 7d882ffc1..000000000 --- a/internal_packages/thread-list/lib/thread-list-item-mixin.cjsx +++ /dev/null @@ -1,42 +0,0 @@ -_ = require 'underscore-plus' -moment = require "moment" -{Actions} = require 'inbox-exports' - -module.exports = -ThreadListItemMixin = - threadTime: -> - moment(@props.thread.lastMessageTimestamp).format(@_timeFormat()) - - _timeFormat: -> - diff = @_today().diff(@props.thread.lastMessageTimestamp, 'days', true) - if diff <= 1 - return "h:mm a" - else if diff > 1 and diff <= 365 - return "MMM D" - else - return "MMM D YYYY" - - # Stubbable for testing. Returns a `moment` - _today: -> moment() - - _subject: -> - str = @props.thread.subject - str = "No Subject" unless str - str - - _snippet: -> - snip = @props.thread?.snippet ? "" - snip = snip.replace(/(\r\n|\n|\r)/gm, "") - if snip.length > 160 - "#{snip.slice(0, Math.min(snip.length, 160))}…" - else snip - - _isStarred: -> - @props.thread.isStarred() - - _toggleStar: -> - @props.thread.toggleStar() - - _onClick: (event) -> - event.preventDefault() - Actions.selectThreadId(@props.thread.id) diff --git a/internal_packages/thread-list/lib/thread-list-mixin.cjsx b/internal_packages/thread-list/lib/thread-list-mixin.cjsx deleted file mode 100644 index 805d99c81..000000000 --- a/internal_packages/thread-list/lib/thread-list-mixin.cjsx +++ /dev/null @@ -1,66 +0,0 @@ -_ = require 'underscore-plus' -{Actions, ThreadStore} = require 'inbox-exports' - -module.exports = -ThreadListMixin = - getInitialState: -> - @_getStateFromStores() - - componentDidMount: -> - @thread_store_unsubscribe = ThreadStore.listen @_onChange - @thread_unsubscriber = atom.commands.add '.thread-list', { - 'thread-list:star-thread': => @_onStarThread() - } - @body_unsubscriber = atom.commands.add 'body', { - 'application:previous-message': => @_onShiftSelectedIndex(-1) - 'application:next-message': => @_onShiftSelectedIndex(1) - 'application:archive-thread': @_onArchiveSelected - 'application:archive-and-previous': @_onArchiveAndPrevious - 'application:reply': @_onReply - 'application:reply-all': @_onReplyAll - 'application:forward': @_onForward - } - - componentWillUnmount: -> - @thread_store_unsubscribe() - @thread_unsubscriber.dispose() - @body_unsubscriber.dispose() - - _onShiftSelectedIndex: (delta) -> - item = _.find @state.threads, (thread) => thread.id == @state?.selected - index = if item then @state.threads.indexOf(item) else -1 - index = Math.max(0, Math.min(index + delta, @state.threads.length-1)) - Actions.selectThreadId(@state.threads[index].id) - - _onArchiveSelected: -> - thread = ThreadStore.selectedThread() - thread.archive() if thread - - _onStarThread: -> - thread = ThreadStore.selectedThread() - thread.toggleStar() if thread - - _onReply: -> - thread = ThreadStore.selectedThread() - Actions.composeReply(threadId: thread.id) if thread? - - _onReplyAll: -> - thread = ThreadStore.selectedThread() - Actions.composeReplyAll(threadId: thread.id) if thread? - - _onForward: -> - thread = ThreadStore.selectedThread() - Actions.composeForward(threadId: thread.id) if thread? - - _onChange: -> - @setState(@_getStateFromStores()) - - _onArchiveAndPrevious: -> - @_onArchiveSelected() - @_onShiftSelectedIndex(-1) - - _getStateFromStores: -> - count: ThreadStore.items().length - threads: ThreadStore.items() - selected: ThreadStore.selectedId() - diff --git a/internal_packages/thread-list/lib/thread-list-tabular-item.cjsx b/internal_packages/thread-list/lib/thread-list-tabular-item.cjsx deleted file mode 100644 index 3ada458db..000000000 --- a/internal_packages/thread-list/lib/thread-list-tabular-item.cjsx +++ /dev/null @@ -1,36 +0,0 @@ -_ = require 'underscore-plus' -React = require 'react/addons' - -ThreadListItemMixin = require './thread-list-item-mixin.cjsx' - -module.exports = -ThreadListTabularItem = React.createClass - displayName: 'ThreadListTabularItem' - mixins: [ThreadListItemMixin] - - render: -> -
- {@_columns()} -
- - # 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) -> - not _.isEqual(@props, nextProps) - - _columns: -> - for column in (@props.columns ? []) -
- {column.resolver(@props.thread, @)} -
- - _containerClasses: -> - React.addons.classSet - 'unread': @props.unread - 'selected': @props.selected - 'thread-list-item': true - 'thread-list-tabular-item': true diff --git a/internal_packages/thread-list/lib/thread-list-tabular.cjsx b/internal_packages/thread-list/lib/thread-list-tabular.cjsx deleted file mode 100644 index 59d8987e8..000000000 --- a/internal_packages/thread-list/lib/thread-list-tabular.cjsx +++ /dev/null @@ -1,136 +0,0 @@ -_ = require 'underscore-plus' -React = require 'react' - -{ComponentRegistry} = require 'inbox-exports' - -ThreadListMixin = require './thread-list-mixin.cjsx' -ThreadListColumn = require("./thread-list-column") -ThreadListTabularItem = require './thread-list-tabular-item.cjsx' - -module.exports = -ThreadListTabular = React.createClass - mixins: [ComponentRegistry.Mixin, ThreadListMixin] - displayName: 'ThreadListTabular' - components: ["Participants"] - - getInitialState: -> - columns: @_defaultColumns() - threadLabelComponents: ComponentRegistry.findAllByRole("thread label") - - componentWillUpdate: -> - @_colFlex = null - - componentWillMount: -> - @unlisteners = [] - @unlisteners.push ComponentRegistry.listen (event) => - @setState - threadLabelComponents: ComponentRegistry.findAllByRole("thread label") - - componentWillUnmount: -> - unlisten() for unlisten in @unlisteners - - render: -> -
-
- -
- {@_threadRows()} -
-
-
- - _defaultColumns: -> - c0 = new ThreadListColumn - name: "★" - flex: 0.2 - resolver: (thread, parentComponent) -> - thread.toggleStar.apply(thread)}> - - - - c1 = new ThreadListColumn - name: "Name" - flex: 2 - resolver: (thread, parentComponent) => - Participants = @state.Participants -
- -
- - subject = (thread) -> - if (thread.subject ? "").trim().length is 0 - return (No Subject) - else return thread.subject - - labelComponents = (thread) => - for label in @state.threadLabelComponents - LabelComponent = label.view - - - numUnread = (thread) -> - numMsg = thread.numUnread() - if numMsg < 2 - - else - {numMsg} - - c2 = new ThreadListColumn - name: "Subject" - flex: 3 - resolver: (thread) -> - - {subject(thread)} - {numUnread(thread)} - - - c3 = new ThreadListColumn - name: "Snippet" - flex: 4 - resolver: (thread) -> - {thread.snippet} - - c4 = new ThreadListColumn - name: "Date" - flex: 1 - resolver: (thread, parentComponent) -> - - {parentComponent.threadTime()} - - - return [c1, c2, c3, c4] - - _threadHeaders: -> - return
- # TODO: There's currently no styling for headers - # for col in @state.columns - #
- # {col.name} - #
- - # The `numTags` attribute is only used to trigger a re-render of the - # ThreadListTabularItem when a tag gets added or removed (like a star). - # React's diffing engine does not detect the change the array nested - # deep inside of the thread and does not call render on the associated - # ThreadListTabularItem. Add the attribute fixes this. - _threadRows: -> - @state.threads.map (thread) => - - - _columnFlex: -> - if @_colFlex? then return @_colFlex - @_colFlex = {} - for col in (@state.columns ? []) - @_colFlex[col.name] = col.flex - return @_colFlex - diff --git a/internal_packages/thread-list/lib/thread-list.cjsx b/internal_packages/thread-list/lib/thread-list.cjsx new file mode 100644 index 000000000..7a21256eb --- /dev/null +++ b/internal_packages/thread-list/lib/thread-list.cjsx @@ -0,0 +1,138 @@ +_ = require 'underscore-plus' +React = require 'react' +{ListTabular} = require 'ui-components' +{timestamp, subject} = require './formatting-utils' +{Actions, ThreadStore, ComponentRegistry} = require 'inbox-exports' + +module.exports = +ThreadList = React.createClass + displayName: 'ThreadList' + + mixins: [ComponentRegistry.Mixin] + components: ['Participants'] + + getInitialState: -> + @_getStateFromStores() + + componentDidMount: -> + @thread_store_unsubscribe = ThreadStore.listen @_onChange + @thread_unsubscriber = atom.commands.add '.thread-list', { + 'thread-list:star-thread': => @_onStarThread() + } + @body_unsubscriber = atom.commands.add 'body', { + 'application:previous-item': => @_onShiftSelectedIndex(-1) + 'application:next-item': => @_onShiftSelectedIndex(1) + 'application:remove-item': @_onArchiveSelected + 'application:remove-and-previous': @_onArchiveAndPrevious + 'application:reply': @_onReply + 'application:reply-all': @_onReplyAll + 'application:forward': @_onForward + } + + componentWillUnmount: -> + @thread_store_unsubscribe() + @thread_unsubscriber.dispose() + @body_unsubscriber.dispose() + + render: -> +
+ if item.isUnread() then 'unread' else '' } + selectedId={@state.selectedId} + onSelect={ (item) -> Actions.selectThreadId(item.id) } /> +
+ + _computeColumns: -> + labelComponents = (thread) => + for label in @state.threadLabelComponents + LabelComponent = label.view + + + numUnread = (thread) -> + numMsg = thread.numUnread() + if numMsg < 2 + + else + {numMsg} + + c0 = new ListTabular.Column + name: "★" + flex: 0.2 + resolver: (thread) -> + thread.toggleStar.apply(thread)}> + + + + c1 = new ListTabular.Column + name: "Name" + flex: 2 + resolver: (thread) => + Participants = @state.Participants +
+ +
+ + c2 = new ListTabular.Column + name: "Subject" + flex: 3 + resolver: (thread) -> + + {subject(thread.subject)} + {numUnread(thread)} + + + c3 = new ListTabular.Column + name: "Snippet" + flex: 4 + resolver: (thread) -> + {thread.snippet} + + c4 = new ListTabular.Column + name: "Date" + flex: 1 + resolver: (thread) -> + {timestamp(thread.lastMessageTimestamp)} + + [c1, c2, c3, c4] + + _onShiftSelectedIndex: (delta) -> + item = _.find @state.items, (thread) => thread.id == @state.selectedId + index = if item then @state.items.indexOf(item) else -1 + index = Math.max(0, Math.min(index + delta, @state.items.length-1)) + Actions.selectThreadId(@state.items[index].id) + + _onArchiveSelected: -> + thread = ThreadStore.selectedThread() + thread.archive() if thread + + _onStarThread: -> + thread = ThreadStore.selectedThread() + thread.toggleStar() if thread + + _onReply: -> + return unless @state.selectedId? + Actions.composeReply(threadId: @state.selectedId) + + _onReplyAll: -> + return unless @state.selectedId? + Actions.composeReplyAll(threadId: @state.selectedId) + + _onForward: -> + return unless @state.selectedId? + Actions.composeForward(threadId: @state.selectedId) + + _onChange: -> + @setState(@_getStateFromStores()) + + _onArchiveAndPrevious: -> + @_onArchiveSelected() + @_onShiftSelectedIndex(-1) + + _getStateFromStores: -> + items: ThreadStore.items() + columns: @_computeColumns() + selectedId: ThreadStore.selectedId() diff --git a/internal_packages/thread-list/spec/thread-list-spec.cjsx b/internal_packages/thread-list/spec/thread-list-spec.cjsx index 46d9f1b60..ca492a5b2 100644 --- a/internal_packages/thread-list/spec/thread-list-spec.cjsx +++ b/internal_packages/thread-list/spec/thread-list-spec.cjsx @@ -3,7 +3,7 @@ _ = require 'underscore-plus' CSON = require 'season' React = require "react/addons" ReactTestUtils = React.addons.TestUtils -ReactTestUtils = _.extend ReactTestUtils, require("jasmine-react-helpers") +ReactTestUtils = _.extend ReactTestUtils, require "jasmine-react-helpers" {Thread, Actions, @@ -13,14 +13,9 @@ ReactTestUtils = _.extend ReactTestUtils, require("jasmine-react-helpers") InboxTestUtils, NamespaceStore, ComponentRegistry} = require "inbox-exports" +{ListTabular} = require 'ui-components' -ThreadListColumn = require("../lib/thread-list-column") - -ThreadListNarrow = require("../lib/thread-list-narrow.cjsx") -ThreadListNarrowItem = require("../lib/thread-list-narrow-item.cjsx") - -ThreadListTabular = require("../lib/thread-list-tabular.cjsx") -ThreadListTabularItem = require("../lib/thread-list-tabular-item.cjsx") +ThreadList = require "../lib/thread-list.cjsx" ParticipantsItem = React.createClass render: ->
@@ -195,18 +190,18 @@ cjsxSubjectResolver = (thread) -> Snippet
-describe "ThreadListTabular", -> +describe "ThreadList", -> Foo = React.createClass({render: ->
{@props.children}
}) - c1 = new ThreadListColumn + c1 = new ListTabular.Column name: "Name" flex: 1 resolver: (thread) -> "#{thread.id} Test Name" - c2 = new ThreadListColumn + c2 = new ListTabular.Column name: "Subject" flex: 3 resolver: cjsxSubjectResolver - c3 = new ThreadListColumn + c3 = new ListTabular.Column name: "Date" resolver: (thread) -> {thread.id} @@ -217,8 +212,7 @@ describe "ThreadListTabular", -> spyOn(ThreadStore, "_onNamespaceChanged") spyOn(DatabaseStore, "findAll").andCallFake -> new Promise (resolve, reject) -> resolve(test_threads()) - ReactTestUtils.spyOnClass(ThreadListTabular, "_defaultColumns") - .andReturn(columns) + ReactTestUtils.spyOnClass(ThreadList, "_computeColumns").andReturn(columns) ThreadStore._resetInstanceVars() @@ -227,12 +221,12 @@ describe "ThreadListTabular", -> view: ParticipantsItem @thread_list = ReactTestUtils.renderIntoDocument( - + ) it "renders into the document", -> expect(ReactTestUtils.isCompositeComponentWithType(@thread_list, - ThreadListTabular)).toBe true + ThreadList)).toBe true it "stars on keymap", -> spyOn(@thread_list, "_onStarThread") @@ -243,8 +237,7 @@ describe "ThreadListTabular", -> expect(@thread_list.state.columns).toEqual columns it "by default has zero children", -> - items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, - ThreadListTabularItem) + items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, ListTabular.Item) expect(items.length).toBe 0 describe "Populated thread list", -> @@ -255,184 +248,143 @@ describe "ThreadListTabular", -> @thread_list_node = @thread_list.getDOMNode() it "renders all of the thread list items", -> - items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, - ThreadListTabularItem) + items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, ListTabular.Item) expect(items.length).toBe 3 - # We no longer put headers in the thread list (for now) - # it "Expects there to be headers", -> - # heads = ReactTestUtils.scryRenderedDOMComponentsWithClass(@thread_list, "thread-list-header") - # expect(heads.length).toBe(3) - # - describe "ThreadListTabularItem", -> - beforeEach -> - items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, - ThreadListTabularItem) - item = items.filter (tli) -> tli.props.thread.id is "111" - @thread_list_item = item[0] +# describe "ThreadListNarrow", -> - it "finds the thread list item by id", -> - expect(@thread_list_item.props.thread.id).toBe "111" +# beforeEach -> +# InboxTestUtils.loadKeymap("internal_packages/thread-list/keymaps/thread-list.cson") +# spyOn(ThreadStore, "_onNamespaceChanged") +# spyOn(DatabaseStore, "findAll").andCallFake -> +# new Promise (resolve, reject) -> resolve(test_threads()) +# ThreadStore._resetInstanceVars() - it "Expects each thread list item to get the column list", -> - expect(@thread_list_item.props.columns).toEqual columns +# ComponentRegistry.register +# name: 'Participants' +# view: ParticipantsItem - it "has the proper column widths", -> - expect(@thread_list_item.props.columnFlex["Name"]).toEqual 1 - expect(@thread_list_item.props.columnFlex["Subject"]).toEqual 3 - expect(@thread_list_item.props.columnFlex["Date"]).toEqual undefined +# @thread_list = ReactTestUtils.renderIntoDocument( +# +# ) - describe "columns in thread list item", -> - beforeEach -> - @cols = ReactTestUtils.scryRenderedDOMComponentsWithClass(@thread_list_item, "thread-list-column") +# it "renders into the document", -> +# expect(ReactTestUtils.isCompositeComponentWithType(@thread_list, +# ThreadListNarrow)).toBe true - it "Expects there to be three columns", -> - expect(@cols.length).toBe(3) +# it "by default has zero children", -> +# items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, +# ThreadListNarrowItem) +# expect(items.length).toBe 0 - it "Expects the correct test data in the columns", -> - snip1 = ReactTestUtils.scryRenderedDOMComponentsWithClass(@cols[1], "snippet") - snip2 = ReactTestUtils.scryRenderedComponentsWithType(@cols[2], Foo) +# describe "Populated thread list", -> +# beforeEach -> +# ThreadStore._items = test_threads() +# ThreadStore._selectedId = null +# ThreadStore.trigger() +# @thread_list_node = @thread_list.getDOMNode() - expect(@cols[0].props.children).toBe "111 Test Name" - expect(snip1.length).toBe 1 - expect(snip1[0].props.children).toEqual "Snippet" - expect(snip2.length).toBe 1 - expect(snip2[0].props.children).toEqual "111" +# it "renders all of the thread list items", -> +# items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, +# ThreadListNarrowItem) +# expect(items.length).toBe 3 -describe "ThreadListNarrow", -> +# describe "Shifting selected index", -> - beforeEach -> - InboxTestUtils.loadKeymap("internal_packages/thread-list/keymaps/thread-list.cson") - spyOn(ThreadStore, "_onNamespaceChanged") - spyOn(DatabaseStore, "findAll").andCallFake -> - new Promise (resolve, reject) -> resolve(test_threads()) - ThreadStore._resetInstanceVars() +# beforeEach -> +# spyOn(@thread_list, "_onShiftSelectedIndex") +# spyOn(Actions, "selectThreadId") - ComponentRegistry.register - name: 'Participants' - view: ParticipantsItem +# it "can move selection up", -> +# atom.commands.dispatch(document.body, "application:previous-item") +# expect(@thread_list._onShiftSelectedIndex).toHaveBeenCalledWith(-1) - @thread_list = ReactTestUtils.renderIntoDocument( - - ) +# it "can move selection down", -> +# atom.commands.dispatch(document.body, "application:next-item") +# expect(@thread_list._onShiftSelectedIndex).toHaveBeenCalledWith(1) - it "renders into the document", -> - expect(ReactTestUtils.isCompositeComponentWithType(@thread_list, - ThreadListNarrow)).toBe true +# describe "Triggering message list commands", -> +# beforeEach -> +# spyOn(Actions, "composeReply") +# spyOn(Actions, "composeReplyAll") +# spyOn(Actions, "composeForward") +# ThreadStore._onSelectThreadId("111") +# @thread = ThreadStore.selectedThread() +# spyOn(@thread, "archive") +# spyOn(@thread_list, "_onShiftSelectedIndex") +# spyOn(Actions, "selectThreadId") - it "by default has zero children", -> - items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, - ThreadListNarrowItem) - expect(items.length).toBe 0 +# it "can reply to the currently selected thread", -> +# atom.commands.dispatch(document.body, "application:reply") +# expect(Actions.composeReply).toHaveBeenCalledWith(threadId: @thread.id) - describe "Populated thread list", -> - beforeEach -> - ThreadStore._items = test_threads() - ThreadStore._selectedId = null - ThreadStore.trigger() - @thread_list_node = @thread_list.getDOMNode() +# it "can reply all to the currently selected thread", -> +# atom.commands.dispatch(document.body, "application:reply-all") +# expect(Actions.composeReplyAll).toHaveBeenCalledWith(threadId: @thread.id) - it "renders all of the thread list items", -> - items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, - ThreadListNarrowItem) - expect(items.length).toBe 3 +# it "can forward the currently selected thread", -> +# atom.commands.dispatch(document.body, "application:forward") +# expect(Actions.composeForward).toHaveBeenCalledWith(threadId: @thread.id) - describe "Shifting selected index", -> +# it "can archive the currently selected thread", -> +# atom.commands.dispatch(document.body, "application:remove-item") +# expect(@thread.archive).toHaveBeenCalled() - beforeEach -> - spyOn(@thread_list, "_onShiftSelectedIndex") - spyOn(Actions, "selectThreadId") +# it "can archive the currently selected thread and navigate up", -> +# atom.commands.dispatch(document.body, "application:remove-and-previous") +# expect(@thread.archive).toHaveBeenCalled() +# expect(@thread_list._onShiftSelectedIndex).toHaveBeenCalledWith(-1) - it "can move selection up", -> - atom.commands.dispatch(document.body, "application:previous-message") - expect(@thread_list._onShiftSelectedIndex).toHaveBeenCalledWith(-1) +# it "does nothing when no thread is selected", -> +# ThreadStore._selectedId = null +# atom.commands.dispatch(document.body, "application:reply") +# expect(Actions.composeReply.calls.length).toEqual(0) - it "can move selection down", -> - atom.commands.dispatch(document.body, "application:next-message") - expect(@thread_list._onShiftSelectedIndex).toHaveBeenCalledWith(1) +# describe "ThreadListNarrowItem", -> +# beforeEach -> +# items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, +# ThreadListNarrowItem) +# item = items.filter (tli) -> tli.props.thread.id is "111" +# @thread_list_item = item[0] +# @thread_date = moment(@thread_list_item.props.thread.lastMessageTimestamp) - describe "Triggering message list commands", -> - beforeEach -> - spyOn(Actions, "composeReply") - spyOn(Actions, "composeReplyAll") - spyOn(Actions, "composeForward") - ThreadStore._onSelectThreadId("111") - @thread = ThreadStore.selectedThread() - spyOn(@thread, "archive") - spyOn(@thread_list, "_onShiftSelectedIndex") - spyOn(Actions, "selectThreadId") +# it "finds the thread list item by id", -> +# expect(@thread_list_item.props.thread.id).toBe "111" - it "can reply to the currently selected thread", -> - atom.commands.dispatch(document.body, "application:reply") - expect(Actions.composeReply).toHaveBeenCalledWith(threadId: @thread.id) +# it "fires the appropriate Action on click", -> +# spyOn(Actions, "selectThreadId") +# ReactTestUtils.Simulate.click @thread_list_item.getDOMNode() +# expect(Actions.selectThreadId).toHaveBeenCalledWith("111") - it "can reply all to the currently selected thread", -> - atom.commands.dispatch(document.body, "application:reply-all") - expect(Actions.composeReplyAll).toHaveBeenCalledWith(threadId: @thread.id) +# it "sets the selected state on the thread item", -> +# ThreadStore._onSelectThreadId("111") +# items = ReactTestUtils.scryRenderedDOMComponentsWithClass(@thread_list, "selected") +# expect(items.length).toBe 1 +# expect(items[0].props.id).toBe "111" - it "can forward the currently selected thread", -> - atom.commands.dispatch(document.body, "application:forward") - expect(Actions.composeForward).toHaveBeenCalledWith(threadId: @thread.id) +# it "renders de-selection when invalid id is emitted", -> +# ThreadStore._onSelectThreadId('abc') +# items = ReactTestUtils.scryRenderedDOMComponentsWithClass(@thread_list, "selected") +# expect(items.length).toBe 0 - it "can archive the currently selected thread", -> - atom.commands.dispatch(document.body, "application:archive-thread") - expect(@thread.archive).toHaveBeenCalled() +# # test "last_message_timestamp": 1415742036 +# it "displays the time from threads LONG ago", -> +# spyOn(@thread_list_item, "_today").andCallFake => +# @thread_date.add(2, 'years') +# expect(@thread_list_item._timeFormat()).toBe "MMM D YYYY" - it "can archive the currently selected thread and navigate up", -> - atom.commands.dispatch(document.body, "application:archive-and-previous") - expect(@thread.archive).toHaveBeenCalled() - expect(@thread_list._onShiftSelectedIndex).toHaveBeenCalledWith(-1) +# it "displays the time from threads a bit ago", -> +# spyOn(@thread_list_item, "_today").andCallFake => +# @thread_date.add(2, 'days') +# expect(@thread_list_item._timeFormat()).toBe "MMM D" - it "does nothing when no thread is selected", -> - ThreadStore._selectedId = null - atom.commands.dispatch(document.body, "application:reply") - expect(Actions.composeReply.calls.length).toEqual(0) +# it "displays the time from threads exactly a day ago", -> +# spyOn(@thread_list_item, "_today").andCallFake => +# @thread_date.add(1, 'day') +# expect(@thread_list_item._timeFormat()).toBe "h:mm a" - describe "ThreadListNarrowItem", -> - beforeEach -> - items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, - ThreadListNarrowItem) - item = items.filter (tli) -> tli.props.thread.id is "111" - @thread_list_item = item[0] - @thread_date = moment(@thread_list_item.props.thread.lastMessageTimestamp) - - it "finds the thread list item by id", -> - expect(@thread_list_item.props.thread.id).toBe "111" - - it "fires the appropriate Action on click", -> - spyOn(Actions, "selectThreadId") - ReactTestUtils.Simulate.click @thread_list_item.getDOMNode() - expect(Actions.selectThreadId).toHaveBeenCalledWith("111") - - it "sets the selected state on the thread item", -> - ThreadStore._onSelectThreadId("111") - items = ReactTestUtils.scryRenderedDOMComponentsWithClass(@thread_list, "selected") - expect(items.length).toBe 1 - expect(items[0].props.id).toBe "111" - - it "renders de-selection when invalid id is emitted", -> - ThreadStore._onSelectThreadId('abc') - items = ReactTestUtils.scryRenderedDOMComponentsWithClass(@thread_list, "selected") - expect(items.length).toBe 0 - - # test "last_message_timestamp": 1415742036 - it "displays the time from threads LONG ago", -> - spyOn(@thread_list_item, "_today").andCallFake => - @thread_date.add(2, 'years') - expect(@thread_list_item._timeFormat()).toBe "MMM D YYYY" - - it "displays the time from threads a bit ago", -> - spyOn(@thread_list_item, "_today").andCallFake => - @thread_date.add(2, 'days') - expect(@thread_list_item._timeFormat()).toBe "MMM D" - - it "displays the time from threads exactly a day ago", -> - spyOn(@thread_list_item, "_today").andCallFake => - @thread_date.add(1, 'day') - expect(@thread_list_item._timeFormat()).toBe "h:mm a" - - it "displays the time from threads recently", -> - spyOn(@thread_list_item, "_today").andCallFake => - @thread_date.add(2, 'hours') - expect(@thread_list_item._timeFormat()).toBe "h:mm a" +# it "displays the time from threads recently", -> +# spyOn(@thread_list_item, "_today").andCallFake => +# @thread_date.add(2, 'hours') +# expect(@thread_list_item._timeFormat()).toBe "h:mm a" diff --git a/internal_packages/thread-list/stylesheets/thread-list.less b/internal_packages/thread-list/stylesheets/thread-list.less index f49878686..958d1c414 100644 --- a/internal_packages/thread-list/stylesheets/thread-list.less +++ b/internal_packages/thread-list/stylesheets/thread-list.less @@ -5,12 +5,11 @@ outline:none; } -#thread-list { +.thread-list { order: 3; flex: 1; overflow: auto; -webkit-user-select: none; - display: flex; position: relative; .message-count { @@ -23,7 +22,10 @@ .participants { font-size: @font-size-large; font-weight: @font-weight-semi-bold; + text-overflow: ellipsis; + overflow: hidden; } + .subject { font-size: @font-size-small; font-weight: @font-weight-normal; @@ -33,6 +35,23 @@ font-weight: @font-weight-normal; color: @text-color-subtle; } + + .unread:not(.selected) { + &:before { + content: ""; + position: absolute; + height: 99%; + width: 5px; + top: 0; + left: 1px; + background: @unread-color; + } + + .subject { + color: @unread-color; + } + } + .selected { .participants { color: @text-color-inverse; @@ -44,47 +63,6 @@ color: @text-color-inverse-subtle; } } -} - -.thread-list-container { - position: relative; - - .thread-list-item { - font-size: @font-size-base; - line-height: @line-height-large; - color: @text-color; - background: @list-bg; - - .participants { - font-weight: 500; - } - - &:hover { - background: @list-hover-bg; - } - - &.unread:not(.selected) { - &:before { - content: ""; - position: absolute; - height: 99%; - width: 5px; - top: 0; - left: 1px; - background: @unread-color; - } - - .subject { - color: @unread-color; - } - - } - - &.selected { - background: @list-active-bg; - color: @list-active-color; - } - } .star-button { font-size: 16px; @@ -105,61 +83,3 @@ } } -.thread-list-tabular { - flex: 1; - width: 100%; - display: flex; - overflow: hidden; - position: relative; - - .thread-list-tabular-item { - position: relative; - width: 100%; - display: flex; - &:hover { - cursor: default; - } - } - - .thread-rows { - overflow: auto; - // Add back when when we re-implement thread-list-headers - // padding-top: @font-size-base * 2; /* height of thread-list-headers*/ - } - - .thread-list-column { - // The width is set by React. - display: inline-block; - padding: @padding-base-vertical @padding-base-horizontal @padding-base-vertical @padding-base-horizontal; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - border-bottom: 1px solid @list-border; - - &:last-child { - text-align: right; - } - } - - .thread-list-headers { - display: flex; - position: absolute; - width: 100%; - top: 0; - position: absolute; - background: fade(@list-bg,90%); - font-size: @font-size-base; - line-height: @font-size-base * 1.6; - height:@font-size-base * 2; - z-index: 3; - } - - .thread-list-header { - } - - .participants { - text-overflow: ellipsis; - overflow: hidden; - } - -} diff --git a/keymaps/base.cson b/keymaps/base.cson index 1116bccbd..7d3758fe3 100644 --- a/keymaps/base.cson +++ b/keymaps/base.cson @@ -6,15 +6,15 @@ 'body': 'c' : 'application:new-message' # Gmail - 'k' : 'application:previous-message' # Gmail - 'up' : 'application:previous-message' # Mac mail - 'j' : 'application:next-message' # Gmail - 'down' : 'application:next-message' # Mac mail - ']' : 'application:archive-and-previous' # Gmail - '[' : 'application:archive-thread' # Gmail - 'e' : 'application:archive-thread' # Gmail - 'delete' : 'application:archive-thread' # Mac mail - 'backspace': 'application:archive-thread' # Outlook + 'k' : 'application:previous-item' # Gmail + 'up' : 'application:previous-item' # Mac mail + 'j' : 'application:next-item' # Gmail + 'down' : 'application:next-item' # Mac mail + ']' : 'application:remove-and-previous' # Gmail + '[' : 'application:remove-item' # Gmail + 'e' : 'application:remove-item' # Gmail + 'delete' : 'application:remove-item' # Mac mail + 'backspace': 'application:remove-item' # Outlook 'r' : 'application:reply' # Gmail 'a' : 'application:reply-all' # Gmail diff --git a/spec-inbox/models/model-spec.coffee b/spec-inbox/models/model-spec.coffee index 1453507e1..8fb14618b 100644 --- a/spec-inbox/models/model-spec.coffee +++ b/spec-inbox/models/model-spec.coffee @@ -22,20 +22,6 @@ describe "Model", -> m = new Model() expect(m.attributes()).toBe(m.constructor.attributes) - describe "isEqual", -> - it "should return true iff the classes and IDs match", -> - class Submodel extends Model - constructor: -> super - - a = new Model({id: "A"}) - b = new Model({id: "B"}) - aSub = new Submodel({id: "A"}) - aEqualSub = new Submodel({id: "A"}) - - expect(a.isEqual(b)).toBe(false) - expect(a.isEqual(aSub)).toBe(false) - expect(aSub.isEqual(aEqualSub)).toBe(true) - describe "isSaved", -> it "should return false if the object has a temp ID", -> a = new Model() diff --git a/src/atom.coffee b/src/atom.coffee index a73f83fa6..0626d4c14 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -624,7 +624,7 @@ class Atom extends Model @displayOnboardingWindow() displayComposer: (draftLocalId = null) -> - ipc.send('show-composer-window', {draftLocalId: draftLocalId}) + ipc.send('show-composer-window', {draftLocalId}) displayOnboardingWindow: (page = false) -> options = diff --git a/src/browser/edgehill-application.coffee b/src/browser/edgehill-application.coffee index 19356f72f..4f3331f3b 100644 --- a/src/browser/edgehill-application.coffee +++ b/src/browser/edgehill-application.coffee @@ -442,7 +442,8 @@ class AtomApplication w.focus() sendComposerState = -> - w.browserWindow.webContents.send 'composer-state', JSON.stringify({draftLocalId, draftInitialJSON}) + json = JSON.stringify({draftLocalId, draftInitialJSON}) + w.browserWindow.webContents.send('composer-state', json) if w.browserWindow.webContents.isLoading() w.browserWindow.webContents.on('did-finish-load', sendComposerState) diff --git a/internal_packages/thread-list/lib/thread-list-narrow-item.cjsx b/src/components/list-narrow-item.cjsx similarity index 100% rename from internal_packages/thread-list/lib/thread-list-narrow-item.cjsx rename to src/components/list-narrow-item.cjsx diff --git a/internal_packages/thread-list/lib/thread-list-narrow.cjsx b/src/components/list-narrow.cjsx similarity index 100% rename from internal_packages/thread-list/lib/thread-list-narrow.cjsx rename to src/components/list-narrow.cjsx diff --git a/src/components/list-tabular.cjsx b/src/components/list-tabular.cjsx new file mode 100644 index 000000000..efe9cbbf0 --- /dev/null +++ b/src/components/list-tabular.cjsx @@ -0,0 +1,99 @@ +_ = require 'underscore-plus' +React = require 'react' + +class ListColumn + constructor: ({@name, @resolver, @flex}) -> + +ListTabularItem = React.createClass + displayName: 'ListTabularItem' + propTypes: + item: React.PropTypes.object + itemClassProvider: React.PropTypes.func + displayHeaders: React.PropTypes.bool + onSelect: 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) -> + not _.isEqual(@props, nextProps) + + render: -> +
+ {@_columns()} +
+ + _columns: -> + for column in (@props.columns ? []) +
+ {column.resolver(@props.item, @)} +
+ + _onClick: -> + if not @props.selected + @props.onSelect?(@props.item) + + if @_lastClickTime? and Date.now() - @_lastClickTime < 350 + @props.onDoubleClick?(@props.item) + + @_lastClickTime = Date.now() + + _containerClasses: -> + classes = @props.itemClassProvider?(@props.item) + classes = '' unless _.isString(classes) + classes += ' ' + React.addons.classSet + 'selected': @props.selected + 'list-item': true + 'list-tabular-item': true + classes + +module.exports = +ListTabular = React.createClass + displayName: 'ListTabular' + propTypes: + columns: React.PropTypes.arrayOf(React.PropTypes.object) + items: React.PropTypes.arrayOf(React.PropTypes.object) + itemClassProvider: React.PropTypes.func + selectedId: React.PropTypes.string + onSelect: React.PropTypes.func + onDoubleClick: React.PropTypes.func + + render: -> +
+ {@_headers()} +
+ {@_rows()} +
+
+ + _headers: -> + return [] unless @props.displayHeaders + + headerColumns = @props.columns.map (column) -> +
+ {column.name} +
+ +
+ {headerColumns} +
+ + _rows: -> + @props.items.map (item) => + + + +ListTabular.Item = ListTabularItem +ListTabular.Column = ListColumn diff --git a/src/flux/action-bridge.coffee b/src/flux/action-bridge.coffee index e585c21a4..45610f40b 100644 --- a/src/flux/action-bridge.coffee +++ b/src/flux/action-bridge.coffee @@ -45,14 +45,14 @@ class ActionBridge callback = => @onRebroadcast(TargetWindows.ALL, name, arguments) Actions[name].listen(callback, @) - if @role == Role.ROOT - # Observe the database store (possibly other stores in the future), and - # rebroadcast it's trigger() event. - callback = (change) => - @onRebroadcast(TargetWindows.ALL, Message.DATABASE_STORE_TRIGGER, [change]) - DatabaseStore.listen(callback, @) + # Observe the database store (possibly other stores in the future), and + # rebroadcast it's trigger() event. + databaseCallback = (change) => + return if DatabaseStore.triggeringFromActionBridge + @onRebroadcast(TargetWindows.ALL, Message.DATABASE_STORE_TRIGGER, [change]) + DatabaseStore.listen(databaseCallback, @) - else + if @role isnt Role.ROOT # Observe all mainWindow actions fired in this window and re-broadcast # them to other windows so the central application stores can take action Actions.mainWindowActions.forEach (name) => @@ -71,8 +71,10 @@ class ActionBridge console.error(e) if name == Message.DATABASE_STORE_TRIGGER - return unless @role == Role.SECONDARY + DatabaseStore.triggeringFromActionBridge = true DatabaseStore.trigger(args...) + DatabaseStore.triggeringFromActionBridge = false + else if Actions[name] Actions[name].firing = true Actions[name](args...) diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index 971f3d5f2..8236c0e3c 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -49,6 +49,7 @@ windowActions = [ "selectNamespaceId", "selectThreadId", "selectTagId", + "selectView", # Actions for composer "composeReply", diff --git a/src/flux/models/model.coffee b/src/flux/models/model.coffee index 5e8e54f15..de7c578a3 100644 --- a/src/flux/models/model.coffee +++ b/src/flux/models/model.coffee @@ -31,9 +31,6 @@ class Model attributes: -> @constructor.attributes - isEqual: (other) -> - other?.id == @id && other?.constructor == @constructor - isSaved: -> !isTempId(@id) diff --git a/src/flux/stores/draft-store-proxy.coffee b/src/flux/stores/draft-store-proxy.coffee index dfced1878..835a0ac71 100644 --- a/src/flux/stores/draft-store-proxy.coffee +++ b/src/flux/stores/draft-store-proxy.coffee @@ -33,13 +33,14 @@ class DraftChangeSet @_timer = setTimeout(@commit, 5000) commit: => - return unless Object.keys(@_pending).length > 0 + if Object.keys(@_pending).length is 0 + return Promise.resolve(true) DatabaseStore = require './database-store' DatabaseStore.findByLocalId(Message, @localId).then (draft) => draft = @applyToModel(draft) - DatabaseStore.persistModel(draft) @_pending = {} + DatabaseStore.persistModel(draft) applyToModel: (model) => model.fromJSON(@_pending) if model diff --git a/src/flux/stores/draft-store.coffee b/src/flux/stores/draft-store.coffee index fc78842ad..d95c8a73b 100644 --- a/src/flux/stores/draft-store.coffee +++ b/src/flux/stores/draft-store.coffee @@ -38,11 +38,46 @@ DraftStore = Reflux.createStore @listenTo Actions.removeFile, @_onRemoveFile @listenTo Actions.attachFileComplete, @_onAttachFileComplete + + @_drafts = [] @_draftSessions = {} + # TODO: Doesn't work if we do window.addEventListener, but this is + # fragile. Pending an Atom fix perhaps? + window.onbeforeunload = (event) => + promises = [] + + # Normally we'd just append all promises, even the ones already + # fulfilled (nothing to save), but in this case we only want to + # block window closing if we have to do real work. Calling + # window.close() within on onbeforeunload could do weird things. + for key, session of @_draftSessions + promise = session.changes.commit() + if not promise.isFulfilled() + promises.push(promise) + + if promises.length > 0 + Promise.settle(promises).then => + @_draftSessions = {} + window.close() + + # Stop and wait before closing + return false + else + # Continue closing + return true + + DatabaseStore.findAll(Message, draft: true).then (drafts) => + @_drafts = drafts + @trigger({}) + ######### PUBLIC ####################################################### # Returns a promise + + items: -> + @_drafts + sessionForLocalId: (localId) -> @_draftSessions[localId] ?= new DraftStoreProxy(localId) @_draftSessions[localId] @@ -53,7 +88,10 @@ DraftStore = Reflux.createStore return unless change.objectClass is Message.name containsDraft = _.some(change.objects, (msg) -> msg.draft) return unless containsDraft - @trigger(change) + + DatabaseStore.findAll(Message, draft: true).then (drafts) => + @_drafts = drafts + @trigger(change) _onComposeReply: (context) -> @_newMessageWithContext context, (thread, message) -> @@ -139,7 +177,7 @@ DraftStore = Reflux.createStore _onSendDraft: (draftLocalId) -> # Immediately save any pending changes so we don't save after sending - save = @_draftSessions[draftLocalId]?.changes.commit() ? Promise.resolve() + save = @_draftSessions[draftLocalId]?.changes.commit() save.then -> # Queue the task to send the draft Actions.queueTask(new SendDraftTask(draftLocalId)) diff --git a/src/flux/stores/workspace-store.coffee b/src/flux/stores/workspace-store.coffee new file mode 100644 index 000000000..987e0c4af --- /dev/null +++ b/src/flux/stores/workspace-store.coffee @@ -0,0 +1,24 @@ +Reflux = require 'reflux' +NamespaceStore = require './namespace-store' +Actions = require '../actions' + +WorkspaceStore = Reflux.createStore + init: -> + @_resetInstanceVars() + @listenTo Actions.selectView, @_onSelectView + + _resetInstanceVars: -> + @_view = 'threads' + + # Inbound Events + + _onSelectView: (view) -> + @_view = view + @trigger(@) + + # Accessing Data + + selectedView: -> + @_view + +module.exports = WorkspaceStore diff --git a/src/sheet-store.cjsx b/src/sheet-store.cjsx index f8d5885ca..4a8b0fb5a 100644 --- a/src/sheet-store.cjsx +++ b/src/sheet-store.cjsx @@ -6,7 +6,7 @@ Sheet = require './sheet' SheetStore = Reflux.createStore init: -> @_stack = [] - @pushSheet() + @pushSheet() @listenTo Actions.popSheet, @popSheet diff --git a/src/sheet.cjsx b/src/sheet.cjsx index 060a79393..1709677c5 100644 --- a/src/sheet.cjsx +++ b/src/sheet.cjsx @@ -93,8 +93,8 @@ Sheet = React.createClass # Load components that are part of our sheet. For each column, # (eg 'Center') we look for items with a matching `role`. We # then pull toolbar items the following places: - # - # - ThreadList:Center:Toolbar + # + # - Root:Center:Toolbar # - ComposeButton:Toolbar # _getComponentRegistryState: -> diff --git a/static/components/list-tabular.less b/static/components/list-tabular.less new file mode 100644 index 000000000..734c20b22 --- /dev/null +++ b/static/components/list-tabular.less @@ -0,0 +1,74 @@ +@import "ui-variables"; + +.list-container { + position: relative; + + .list-item { + font-size: @font-size-base; + line-height: @line-height-large; + color: @text-color; + background: @list-bg; + + &:hover { + background: @list-hover-bg; + } + + &.selected { + background: @list-active-bg; + color: @list-active-color; + } + } +} + +.list-tabular { + flex: 1; + width: 100%; + overflow: hidden; + position: relative; + + .list-tabular-item { + position: relative; + width: 100%; + display: flex; + &:hover { + cursor: default; + } + } + + .list-rows { + overflow: auto; + // Add back when when we re-implement list-headers + // padding-top: @font-size-base * 2; /* height of list-headers*/ + } + + .list-column { + // The width is set by React. + display: inline-block; + padding: @padding-base-vertical @padding-base-horizontal @padding-base-vertical @padding-base-horizontal; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + border-bottom: 1px solid @list-border; + + &:last-child { + text-align: right; + } + } + + .list-headers { + display: flex; + position: absolute; + width: 100%; + top: 0; + position: absolute; + background: fade(@list-bg,90%); + font-size: @font-size-base; + line-height: @font-size-base * 1.6; + height:@font-size-base * 2; + z-index: 3; + } + + .list-header { + } + +} diff --git a/static/index.less b/static/index.less index 4c03a5d27..f27e7ebd8 100644 --- a/static/index.less +++ b/static/index.less @@ -16,3 +16,4 @@ @import "components/menu"; @import "components/tokenizing-text-field"; @import "components/extra"; +@import "components/list-tabular";