fix(empty-states): Use quotes sparingly, show generic empty text in three-column mode and during search
Summary: We now show the inspirational quotes only when in list mode and viewing a tag. When you're viewing search results, or when you're in three-pane mode, you now see a more generic empty state. Test Plan: No tests yet, may want to see if this refactor sticks when we start adding more empty states Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1642
|
@ -11,7 +11,6 @@ module.exports =
|
|||
Popover: require '../src/components/popover'
|
||||
Flexbox: require '../src/components/flexbox'
|
||||
RetinaImg: require '../src/components/retina-img'
|
||||
EmptyState: require '../src/components/empty-state'
|
||||
ListTabular: require '../src/components/list-tabular'
|
||||
DraggableImg: require '../src/components/draggable-img'
|
||||
MultiselectList: require '../src/components/multiselect-list'
|
||||
|
|
|
@ -293,6 +293,7 @@ class MessageList extends React.Component
|
|||
|
||||
_scrollToBottom: =>
|
||||
messageWrap = React.findDOMNode(@refs.messageWrap)
|
||||
return unless messageWrap
|
||||
messageWrap.scrollTop = messageWrap.scrollHeight
|
||||
|
||||
_cacheScrollPos: =>
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
|
||||
&.clear {
|
||||
position: absolute;
|
||||
top: floor(40px - 26px)/2 - 1px;
|
||||
top: 4px;
|
||||
color: @input-cancel-color;
|
||||
right: @padding-base-horizontal;
|
||||
display: none;
|
||||
|
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 286 KiB |
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 389 KiB |
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 409 KiB |
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 280 KiB |
130
internal_packages/thread-list/lib/empty-state.cjsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{DatabaseView,
|
||||
NamespaceStore,
|
||||
NylasAPI,
|
||||
WorkspaceStore} = require 'nylas-exports'
|
||||
|
||||
EmptyMessages = [{
|
||||
"body":"The pessimist complains about the wind.\nThe optimist expects it to change.\nThe realist adjusts the sails."
|
||||
"byline": "- William Arthur Ward"
|
||||
},{
|
||||
"body":"The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart."
|
||||
"byline": "- Hellen Keller"
|
||||
},{
|
||||
"body":"Believe you can and you're halfway there."
|
||||
"byline": "- Theodore Roosevelt"
|
||||
},{
|
||||
"body":"Don't judge each day by the harvest you reap but by the seeds that you plant."
|
||||
"byline": "- Robert Louis Stevenson"
|
||||
}]
|
||||
|
||||
class ContentGeneric extends React.Component
|
||||
render: ->
|
||||
<div className="generic">
|
||||
<div className="message">
|
||||
{@props.messageOverride ? "No threads to display."}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
class ContentQuotes extends React.Component
|
||||
@displayName = 'Quotes'
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = {}
|
||||
|
||||
componentDidMount: ->
|
||||
# Pick a random quote using the day as a seed. I know not all months have
|
||||
# 31 days - this is good enough to generate one quote a day at random!
|
||||
d = new Date()
|
||||
r = d.getDate() + d.getMonth() * 31
|
||||
message = EmptyMessages[r % EmptyMessages.length]
|
||||
@setState(message: message)
|
||||
|
||||
render: ->
|
||||
<div className="quotes">
|
||||
{@_renderMessage()}
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-bottom-left@2x.png" className="bottom-left"/>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-top-left@2x.png" className="top-left"/>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-bottom-right@2x.png" className="bottom-right"/>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-top-right@2x.png" className="top-right"/>
|
||||
</div>
|
||||
|
||||
_renderMessage: ->
|
||||
if @props.messageOverride
|
||||
<div className="message">{@props.messageOverride}</div>
|
||||
else
|
||||
<div className="message">
|
||||
{@state.message?.body}
|
||||
<div className="byline">
|
||||
{@state.message?.byline}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
class EmptyState extends React.Component
|
||||
@displayName = 'EmptyState'
|
||||
@propTypes =
|
||||
visible: React.PropTypes.bool.isRequired
|
||||
dataView: React.PropTypes.object
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
layoutMode: WorkspaceStore.layoutMode()
|
||||
syncing: false
|
||||
active: false
|
||||
|
||||
componentDidMount: ->
|
||||
@_unlisteners = []
|
||||
@_unlisteners.push WorkspaceStore.listen(@_onChange, @)
|
||||
@_unlisteners.push NamespaceStore.listen(@_onNamespacesChanged, @)
|
||||
@_onNamespacesChanged()
|
||||
|
||||
_onNamespacesChanged: ->
|
||||
namespace = NamespaceStore.current()
|
||||
@_worker = NylasAPI.workerForNamespace(namespace)
|
||||
@_workerUnlisten() if @_workerUnlisten
|
||||
@_workerUnlisten = @_worker.listen(@_onChange, @)
|
||||
console.log(@_worker)
|
||||
@setState(syncing: @_worker.busy())
|
||||
|
||||
componentWillUnmount: ->
|
||||
unlisten() for unlisten in @_unlisteners
|
||||
@_workerUnlisten() if @_workerUnlisten
|
||||
|
||||
componentDidUpdate: ->
|
||||
if @props.visible and not @state.active
|
||||
@setState(active:true)
|
||||
|
||||
componentWillReceiveProps: (newProps) ->
|
||||
if newProps.visible is false
|
||||
@setState(active:false)
|
||||
|
||||
render: ->
|
||||
ContentComponent = ContentGeneric
|
||||
messageOverride = null
|
||||
|
||||
if @props.dataView instanceof DatabaseView
|
||||
if @state.layoutMode is 'list'
|
||||
ContentComponent = ContentQuotes
|
||||
if @state.syncing
|
||||
messageOverride = "Please wait while we prepare your mailbox."
|
||||
|
||||
classes = classNames
|
||||
'empty-state': true
|
||||
'visible': @props.visible
|
||||
'active': @state.active
|
||||
|
||||
<div className={classes}>
|
||||
<ContentComponent messageOverride={messageOverride}/>
|
||||
</div>
|
||||
|
||||
_onChange: ->
|
||||
@setState
|
||||
layoutMode: WorkspaceStore.layoutMode()
|
||||
syncing: @_worker.busy()
|
||||
|
||||
|
||||
module.exports = EmptyState
|
|
@ -14,6 +14,8 @@ ThreadListQuickActions = require './thread-list-quick-actions'
|
|||
ThreadListStore = require './thread-list-store'
|
||||
ThreadListIcon = require './thread-list-icon'
|
||||
|
||||
EmptyState = require './empty-state'
|
||||
|
||||
class ThreadListScrollTooltip extends React.Component
|
||||
@displayName: 'ThreadListScrollTooltip'
|
||||
@propTypes:
|
||||
|
@ -128,6 +130,7 @@ class ThreadList extends React.Component
|
|||
'application:reply': @_onReply
|
||||
'application:reply-all': @_onReplyAll
|
||||
'application:forward': @_onForward
|
||||
|
||||
@itemPropsProvider = (item) ->
|
||||
className: classNames
|
||||
'unread': item.isUnread()
|
||||
|
@ -149,6 +152,7 @@ class ThreadList extends React.Component
|
|||
itemHeight={39}
|
||||
className="thread-list"
|
||||
scrollTooltipComponent={ThreadListScrollTooltip}
|
||||
emptyComponent={EmptyState}
|
||||
collection="thread" />
|
||||
else if @state.style is 'narrow'
|
||||
<MultiselectList
|
||||
|
@ -159,6 +163,7 @@ class ThreadList extends React.Component
|
|||
itemHeight={90}
|
||||
className="thread-list thread-list-narrow"
|
||||
scrollTooltipComponent={ThreadListScrollTooltip}
|
||||
emptyComponent={EmptyState}
|
||||
collection="thread" />
|
||||
else
|
||||
<div></div>
|
||||
|
|
|
@ -13,10 +13,33 @@
|
|||
overflow:hidden;
|
||||
|
||||
> div {
|
||||
opacity:0;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity @duration ease-out;
|
||||
width:100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Generic Mode
|
||||
.generic {
|
||||
text-align: center;
|
||||
|
||||
.message {
|
||||
color: @text-color-very-subtle;
|
||||
font-size: 28px;
|
||||
font-weight: @font-weight-blond;
|
||||
text-align: center;
|
||||
top:45%;
|
||||
left:50%;
|
||||
width:80%;
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
|
||||
// Quotes Mode
|
||||
|
||||
.quotes {
|
||||
min-width: 840px;
|
||||
min-height: 650px;
|
||||
position: absolute;
|
||||
|
@ -113,4 +136,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
RetinaImg = require './retina-img'
|
||||
|
||||
EmptyMessages = [{
|
||||
"body":"The pessimist complains about the wind.\nThe optimist expects it to change.\nThe realist adjusts the sails."
|
||||
"byline": "- William Arthur Ward"
|
||||
},{
|
||||
"body":"The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart."
|
||||
"byline": "- Hellen Keller"
|
||||
},{
|
||||
"body":"Believe you can and you're halfway there."
|
||||
"byline": "- Theodore Roosevelt"
|
||||
},{
|
||||
"body":"Don't judge each day by the harvest you reap but by the seeds that you plant."
|
||||
"byline": "- Robert Louis Stevenson"
|
||||
}]
|
||||
|
||||
class EmptyState extends React.Component
|
||||
@displayName = 'EmptyState'
|
||||
@propTypes =
|
||||
visible: React.PropTypes.bool.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
active: false
|
||||
|
||||
componentDidUpdate: ->
|
||||
if @props.visible and not @state.active
|
||||
# Pick a random quote using the day as a seed. I know not all months have
|
||||
# 31 days - this is good enough to generate one quote a day at random!
|
||||
d = new Date()
|
||||
r = d.getDate() + d.getMonth() * 31
|
||||
message = EmptyMessages[r % EmptyMessages.length]
|
||||
@setState(active:true, message: message)
|
||||
|
||||
componentWillReceiveProps: (newProps) ->
|
||||
if newProps.visible is false
|
||||
@setState(active:false)
|
||||
|
||||
render: ->
|
||||
classes = classNames
|
||||
'empty-state': true
|
||||
'visible': @props.visible
|
||||
'active': @state.active
|
||||
|
||||
<div className={classes}>
|
||||
<div>
|
||||
<div className="message">
|
||||
{@state.message?.body}
|
||||
<div className="byline">
|
||||
{@state.message?.byline}
|
||||
</div>
|
||||
</div>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} name="blank-bottom-left.png" className="bottom-left"/>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} name="blank-top-left.png" className="top-left"/>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} name="blank-bottom-right.png" className="bottom-right"/>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentLight} name="blank-top-right.png" className="top-right"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
module.exports = EmptyState
|
|
@ -2,7 +2,6 @@ _ = require 'underscore'
|
|||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
ListTabular = require './list-tabular'
|
||||
EmptyState = require './empty-state'
|
||||
Spinner = require './spinner'
|
||||
{Actions,
|
||||
Utils,
|
||||
|
@ -37,6 +36,7 @@ class MultiselectList extends React.Component
|
|||
itemPropsProvider: React.PropTypes.func.isRequired
|
||||
itemHeight: React.PropTypes.number.isRequired
|
||||
scrollTooltipComponent: React.PropTypes.func
|
||||
emptyComponent: React.PropTypes.func
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
@ -107,6 +107,12 @@ class MultiselectList extends React.Component
|
|||
'keyboard-cursor': @state.handler.shouldShowKeyboardCursor() and item.id is @state.keyboardCursorId
|
||||
props
|
||||
|
||||
emptyElement = []
|
||||
if @props.emptyComponent
|
||||
emptyElement = <@props.emptyComponent
|
||||
visible={@state.ready && @state.dataView.count() is 0}
|
||||
dataView={@state.dataView} />
|
||||
|
||||
if @state.dataView
|
||||
<div className={className}>
|
||||
<ListTabular
|
||||
|
@ -119,7 +125,7 @@ class MultiselectList extends React.Component
|
|||
onSelect={@_onClickItem}
|
||||
onDoubleClick={@props.onDoubleClick} />
|
||||
<Spinner visible={!@state.ready} />
|
||||
<EmptyState visible={@state.ready && @state.dataView.count() is 0} />
|
||||
{emptyElement}
|
||||
</div>
|
||||
else
|
||||
<div className={className}>
|
||||
|
|
|
@ -33,11 +33,17 @@ class NylasSyncWorker
|
|||
state: ->
|
||||
@_state
|
||||
|
||||
busy: ->
|
||||
for key, state of @_state
|
||||
if state.busy
|
||||
return true
|
||||
false
|
||||
|
||||
start: ->
|
||||
@_resumeTimer = setInterval(@resumeFetches, 20000)
|
||||
@_connection.start()
|
||||
@resumeFetches()
|
||||
|
||||
|
||||
cleanup: ->
|
||||
clearInterval(@_resumeTimer)
|
||||
@_connection.end()
|
||||
|
@ -64,7 +70,7 @@ class NylasSyncWorker
|
|||
|
||||
@fetchCollectionCount(model)
|
||||
@fetchCollectionPage(model, {offset: 0, limit: PAGE_SIZE})
|
||||
|
||||
|
||||
fetchCollectionCount: (model) ->
|
||||
@_api.makeRequest
|
||||
path: "/n/#{@_namespaceId}/#{model}"
|
||||
|
|
|
@ -187,8 +187,9 @@ class WorkspaceStore
|
|||
# Return to the root sheet. This method triggers, allowing observers
|
||||
# to update.
|
||||
popToRootSheet: =>
|
||||
@_sheetStack.length = 1
|
||||
@trigger()
|
||||
if @_sheetStack.length > 1
|
||||
@_sheetStack.length = 1
|
||||
@trigger()
|
||||
|
||||
triggerDebounced: _.debounce(( -> @trigger(@)), 1)
|
||||
|
||||
|
|
|
@ -21,5 +21,4 @@
|
|||
@import "components/scroll-region";
|
||||
@import "components/spinner";
|
||||
@import "components/generated-form";
|
||||
@import "components/empty-state";
|
||||
@import "components/unsafe";
|
||||
|
|