mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-29 03:43:16 +08:00
feat(sidebar): Add sidebar controls to add and remove categories
Summary: - Refactors account-sidebar internal package: - Separates into smaller react components - Makes DisclosureTriangle its own independent component - Adds data to AccountSidebarStore to allow removal or addition of items for a specific section of the sidebar - Adds button and input and css styles to create categories - Adds context menu to destroy a category - Adds new method to CategoryStore to get the icon name for the categories of the current account - Removes some unused code Test Plan: Manual Reviewers: evan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2283
This commit is contained in:
parent
6aeda7583b
commit
524028e257
20 changed files with 467 additions and 119 deletions
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -1,18 +1,80 @@
|
||||||
React = require 'react'
|
React = require 'react'
|
||||||
{Actions} = require("nylas-exports")
|
{WorkspaceStore} = require 'nylas-exports'
|
||||||
|
SidebarSheetItem = require './account-sidebar-sheet-item'
|
||||||
|
AccountSidebarMailViewItem = require './account-sidebar-mail-view-item'
|
||||||
|
{DisclosureTriangle} = require 'nylas-component-kit'
|
||||||
|
|
||||||
|
|
||||||
class AccountSidebarItem extends React.Component
|
class AccountSidebarItem extends React.Component
|
||||||
@displayName: "AccountSidebarItem"
|
@displayName: "AccountSidebarItem"
|
||||||
|
|
||||||
render: =>
|
@propTypes: {
|
||||||
className = "item " + if @props.select then " selected" else ""
|
item: React.PropTypes.object.isRequired
|
||||||
<div className={className} onClick={@_onClick} id={@props.item.id}>
|
selected: React.PropTypes.object.isRequired
|
||||||
<div className="name">{@props.item.displayName}</div>
|
onToggleCollapsed: React.PropTypes.func.isRequired
|
||||||
</div>
|
collapsed: React.PropTypes.bool
|
||||||
|
onDestroyItem: React.PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
_onClick: (event) =>
|
@defaultProps: {
|
||||||
event.preventDefault()
|
collapsed: false
|
||||||
Actions.selectView(@props.item.view)
|
}
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
if @props.onDestroyItem?
|
||||||
|
React.findDOMNode(@).addEventListener('contextmenu', @_onShowContextMenu)
|
||||||
|
|
||||||
|
componentWillUnmount: ->
|
||||||
|
if @props.onDestroyItem?
|
||||||
|
React.findDOMNode(@).removeEventListener('contextmenu', @_onShowContextMenu)
|
||||||
|
|
||||||
|
_itemComponent: (item) ->
|
||||||
|
unless item instanceof WorkspaceStore.SidebarItem
|
||||||
|
throw new Error("AccountSidebar:_itemComponents: sections contained an \
|
||||||
|
item which was not a SidebarItem")
|
||||||
|
if item.component
|
||||||
|
Component = item.component
|
||||||
|
<Component
|
||||||
|
item={item}
|
||||||
|
select={item.id is @props.selected?.id } />
|
||||||
|
|
||||||
|
else if item.mailViewFilter
|
||||||
|
<AccountSidebarMailViewItem
|
||||||
|
item={item}
|
||||||
|
mailView={item.mailViewFilter}
|
||||||
|
select={item.mailViewFilter.isEqual(@props.selected)} />
|
||||||
|
|
||||||
|
else if item.sheet
|
||||||
|
<SidebarSheetItem
|
||||||
|
item={item}
|
||||||
|
select={item.sheet.id is @props.selected?.id} />
|
||||||
|
|
||||||
|
else
|
||||||
|
throw new Error("AccountSidebarItem: each item must have a \
|
||||||
|
custom component, or a sheet or mailViewFilter")
|
||||||
|
|
||||||
|
_onShowContextMenu: =>
|
||||||
|
item = @props.item
|
||||||
|
label = item.name
|
||||||
|
remote = require 'remote'
|
||||||
|
Menu = remote.require 'menu'
|
||||||
|
MenuItem = remote.require 'menu-item'
|
||||||
|
|
||||||
|
menu = new Menu()
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: "Delete #{label}"
|
||||||
|
click: => @props.onDestroyItem?(item)
|
||||||
|
}))
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
item = @props.item
|
||||||
|
<span className="item-container">
|
||||||
|
<DisclosureTriangle
|
||||||
|
collapsed={@props.collapsed}
|
||||||
|
visible={item.children.length > 0}
|
||||||
|
onToggleCollapsed={ => @props.onToggleCollapsed(item.id)} />
|
||||||
|
{@_itemComponent(item)}
|
||||||
|
</span>
|
||||||
|
|
||||||
module.exports = AccountSidebarItem
|
module.exports = AccountSidebarItem
|
||||||
|
|
|
@ -2,7 +2,6 @@ React = require 'react'
|
||||||
classNames = require 'classnames'
|
classNames = require 'classnames'
|
||||||
{Actions,
|
{Actions,
|
||||||
Utils,
|
Utils,
|
||||||
ThreadCountsStore,
|
|
||||||
WorkspaceStore,
|
WorkspaceStore,
|
||||||
AccountStore,
|
AccountStore,
|
||||||
FocusedMailViewStore,
|
FocusedMailViewStore,
|
||||||
|
@ -17,7 +16,6 @@ class AccountSidebarMailViewItem extends React.Component
|
||||||
@propTypes:
|
@propTypes:
|
||||||
select: React.PropTypes.bool
|
select: React.PropTypes.bool
|
||||||
item: React.PropTypes.object.isRequired
|
item: React.PropTypes.object.isRequired
|
||||||
itemUnreadCount: React.PropTypes.number
|
|
||||||
mailView: React.PropTypes.object.isRequired
|
mailView: React.PropTypes.object.isRequired
|
||||||
|
|
||||||
constructor: (@props) ->
|
constructor: (@props) ->
|
||||||
|
@ -27,10 +25,13 @@ class AccountSidebarMailViewItem extends React.Component
|
||||||
!Utils.isEqualReact(@props, nextProps) or !Utils.isEqualReact(@state, nextState)
|
!Utils.isEqualReact(@props, nextProps) or !Utils.isEqualReact(@state, nextState)
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
|
isDeleted = @props.mailView?.category?.isDeleted is true
|
||||||
|
|
||||||
containerClass = classNames
|
containerClass = classNames
|
||||||
'item': true
|
'item': true
|
||||||
'selected': @props.select
|
'selected': @props.select
|
||||||
'dropping': @state.isDropping
|
'dropping': @state.isDropping
|
||||||
|
'deleted': isDeleted
|
||||||
|
|
||||||
<DropZone className={containerClass}
|
<DropZone className={containerClass}
|
||||||
onClick={@_onClick}
|
onClick={@_onClick}
|
||||||
|
@ -44,10 +45,10 @@ class AccountSidebarMailViewItem extends React.Component
|
||||||
</DropZone>
|
</DropZone>
|
||||||
|
|
||||||
_renderUnreadCount: =>
|
_renderUnreadCount: =>
|
||||||
return false if @props.itemUnreadCount is 0
|
return false unless @props.item.unreadCount
|
||||||
className = 'item-count-box '
|
className = 'item-count-box '
|
||||||
className += @props.mailView.category?.name
|
className += @props.mailView.category?.name
|
||||||
<div className={className}>{@props.itemUnreadCount}</div>
|
<div className={className}>{@props.item.unreadCount}</div>
|
||||||
|
|
||||||
_renderIcon: ->
|
_renderIcon: ->
|
||||||
<RetinaImg name={@props.mailView.iconName} fallback={'folder.png'} mode={RetinaImg.Mode.ContentIsMask} />
|
<RetinaImg name={@props.mailView.iconName} fallback={'folder.png'} mode={RetinaImg.Mode.ContentIsMask} />
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
React = require 'react'
|
||||||
|
_ = require 'underscore'
|
||||||
|
_str = require 'underscore.string'
|
||||||
|
{RetinaImg} = require 'nylas-component-kit'
|
||||||
|
AccountSidebarItem = require './account-sidebar-item'
|
||||||
|
|
||||||
|
|
||||||
|
class AccountSidebarSection extends React.Component
|
||||||
|
@displayName: "AccountSidebarSection"
|
||||||
|
|
||||||
|
@propTypes: {
|
||||||
|
section: React.PropTypes.object.isRequired
|
||||||
|
collapsed: React.PropTypes.object.isRequired
|
||||||
|
selected: React.PropTypes.object.isRequired
|
||||||
|
onToggleCollapsed: React.PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor: (@props) ->
|
||||||
|
@state = {showCreateInput: false}
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
section = @props.section
|
||||||
|
showInput = @state.showCreateInput
|
||||||
|
allowCreate = section.createItem?
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div className="heading">{section.label}</div>
|
||||||
|
{@_createItemButton(section) if allowCreate}
|
||||||
|
{@_createItemInput(section) if allowCreate and showInput}
|
||||||
|
{@_itemComponents(section.items)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
_createItemButton: ({label}) ->
|
||||||
|
<div
|
||||||
|
className="add-item-button"
|
||||||
|
onClick={@_onCreateButtonClicked.bind(@, label)}>
|
||||||
|
<RetinaImg
|
||||||
|
url="nylas://account-sidebar/assets/icon-sidebar-addcategory@2x.png"
|
||||||
|
fallback="icon-sidebar-addcategory.png"
|
||||||
|
style={height: 14, width: 14}
|
||||||
|
mode={RetinaImg.Mode.ContentPreserve} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_createItemInput: (section) ->
|
||||||
|
label = _str.decapitalize section.label[...-1]
|
||||||
|
placeholder = "Create new #{label}"
|
||||||
|
<span className="item-container">
|
||||||
|
<div className="item add-item-container">
|
||||||
|
<div className="icon">
|
||||||
|
<RetinaImg
|
||||||
|
name="#{section.iconName}"
|
||||||
|
fallback="folder.png"
|
||||||
|
mode={RetinaImg.Mode.ContentIsMask} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
tabIndex="1"
|
||||||
|
className="input-bordered add-item-input"
|
||||||
|
autoFocus={true}
|
||||||
|
onKeyDown={_.partial @_onInputKeyDown, _, section}
|
||||||
|
placeholder={placeholder}/>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
_itemComponents: (items) =>
|
||||||
|
components = []
|
||||||
|
|
||||||
|
items.forEach (item) =>
|
||||||
|
components.push(
|
||||||
|
<AccountSidebarItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
collapsed={@props.collapsed[item.id]}
|
||||||
|
selected={@props.selected}
|
||||||
|
onDestroyItem={@props.section.destroyItem}
|
||||||
|
onToggleCollapsed={@props.onToggleCollapsed} />
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.children.length and not @props.collapsed[item.id]
|
||||||
|
components.push(
|
||||||
|
<section key={"#{item.id}-children"}>
|
||||||
|
{@_itemComponents(item.children)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
|
components
|
||||||
|
|
||||||
|
_onCreateButtonClicked: (sectionLabel) =>
|
||||||
|
@setState(showCreateInput: not @state.showCreateInput)
|
||||||
|
|
||||||
|
_onInputKeyDown: (event, section) =>
|
||||||
|
if event.key is 'Escape'
|
||||||
|
@setState(showCreateInput: false)
|
||||||
|
if event.key in ['Enter', 'Return']
|
||||||
|
@props.section.createItem?(event.target.value)
|
||||||
|
@setState(showCreateInput: false)
|
||||||
|
|
||||||
|
module.exports = AccountSidebarSection
|
|
@ -4,6 +4,7 @@ _ = require 'underscore'
|
||||||
DatabaseStore,
|
DatabaseStore,
|
||||||
CategoryStore,
|
CategoryStore,
|
||||||
AccountStore,
|
AccountStore,
|
||||||
|
ThreadCountsStore,
|
||||||
WorkspaceStore,
|
WorkspaceStore,
|
||||||
DraftCountStore,
|
DraftCountStore,
|
||||||
Actions,
|
Actions,
|
||||||
|
@ -12,7 +13,8 @@ _ = require 'underscore'
|
||||||
Message,
|
Message,
|
||||||
MailViewFilter,
|
MailViewFilter,
|
||||||
FocusedMailViewStore,
|
FocusedMailViewStore,
|
||||||
NylasAPI,
|
SyncbackCategoryTask,
|
||||||
|
DestroyCategoryTask,
|
||||||
Thread} = require 'nylas-exports'
|
Thread} = require 'nylas-exports'
|
||||||
|
|
||||||
class AccountSidebarStore extends NylasStore
|
class AccountSidebarStore extends NylasStore
|
||||||
|
@ -38,7 +40,9 @@ class AccountSidebarStore extends NylasStore
|
||||||
@listenTo CategoryStore, @_refreshSections
|
@listenTo CategoryStore, @_refreshSections
|
||||||
@listenTo WorkspaceStore, @_refreshSections
|
@listenTo WorkspaceStore, @_refreshSections
|
||||||
@listenTo DraftCountStore, @_refreshSections
|
@listenTo DraftCountStore, @_refreshSections
|
||||||
|
@listenTo ThreadCountsStore, @_refreshSections
|
||||||
@listenTo FocusedMailViewStore, => @trigger()
|
@listenTo FocusedMailViewStore, => @trigger()
|
||||||
|
@configSubscription = NylasEnv.config.observe('core.workspace.showUnreadForAllCategories', @_refreshSections)
|
||||||
|
|
||||||
_refreshSections: =>
|
_refreshSections: =>
|
||||||
account = AccountStore.current()
|
account = AccountStore.current()
|
||||||
|
@ -111,6 +115,9 @@ class AccountSidebarStore extends NylasStore
|
||||||
@_sections.push
|
@_sections.push
|
||||||
label: CategoryStore.categoryLabel()
|
label: CategoryStore.categoryLabel()
|
||||||
items: userCategoryItemsHierarchical
|
items: userCategoryItemsHierarchical
|
||||||
|
iconName: CategoryStore.categoryIconName()
|
||||||
|
createItem: @_createCategory
|
||||||
|
destroyItem: @_destroyCategory
|
||||||
|
|
||||||
@trigger()
|
@trigger()
|
||||||
|
|
||||||
|
@ -125,5 +132,23 @@ class AccountSidebarStore extends NylasStore
|
||||||
id: category.id,
|
id: category.id,
|
||||||
name: shortenedName || category.displayName
|
name: shortenedName || category.displayName
|
||||||
mailViewFilter: MailViewFilter.forCategory(category)
|
mailViewFilter: MailViewFilter.forCategory(category)
|
||||||
|
unreadCount: @_itemUnreadCount(category)
|
||||||
|
|
||||||
|
_createCategory: (displayName) ->
|
||||||
|
CategoryClass = AccountStore.current().categoryClass()
|
||||||
|
category = new CategoryClass
|
||||||
|
displayName: displayName
|
||||||
|
accountId: AccountStore.current().id
|
||||||
|
Actions.queueTask(new SyncbackCategoryTask({category}))
|
||||||
|
|
||||||
|
_destroyCategory: (sidebarItem) ->
|
||||||
|
category = sidebarItem.mailViewFilter.category
|
||||||
|
Actions.queueTask(new DestroyCategoryTask({category}))
|
||||||
|
|
||||||
|
_itemUnreadCount: (category) =>
|
||||||
|
unreadCountEnabled = NylasEnv.config.get('core.workspace.showUnreadForAllCategories')
|
||||||
|
if category and (category.name is 'inbox' or unreadCountEnabled)
|
||||||
|
return ThreadCountsStore.unreadCountForCategoryId(category.id)
|
||||||
|
return 0
|
||||||
|
|
||||||
module.exports = new AccountSidebarStore()
|
module.exports = new AccountSidebarStore()
|
||||||
|
|
|
@ -1,26 +1,8 @@
|
||||||
React = require 'react'
|
React = require 'react'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
{Actions, MailViewFilter, WorkspaceStore, ThreadCountsStore} = require("nylas-exports")
|
{ScrollRegion} = require 'nylas-component-kit'
|
||||||
{ScrollRegion, Flexbox} = require("nylas-component-kit")
|
AccountSidebarStore = require './account-sidebar-store'
|
||||||
SidebarDividerItem = require("./account-sidebar-divider-item")
|
AccountSidebarSection = require './account-sidebar-section'
|
||||||
SidebarSheetItem = require("./account-sidebar-sheet-item")
|
|
||||||
AccountSidebarStore = require ("./account-sidebar-store")
|
|
||||||
AccountSidebarMailViewItem = require("./account-sidebar-mail-view-item")
|
|
||||||
{RetinaImg} = require 'nylas-component-kit'
|
|
||||||
|
|
||||||
class DisclosureTriangle extends React.Component
|
|
||||||
@displayName: 'DisclosureTriangle'
|
|
||||||
|
|
||||||
@propTypes:
|
|
||||||
collapsed: React.PropTypes.bool
|
|
||||||
visible: React.PropTypes.bool
|
|
||||||
onToggleCollapsed: React.PropTypes.func.isRequired
|
|
||||||
|
|
||||||
render: ->
|
|
||||||
classnames = "disclosure-triangle"
|
|
||||||
classnames += " visible" if @props.visible
|
|
||||||
classnames += " collapsed" if @props.collapsed
|
|
||||||
<div className={classnames} onClick={@props.onToggleCollapsed}><div></div></div>
|
|
||||||
|
|
||||||
|
|
||||||
class AccountSidebar extends React.Component
|
class AccountSidebar extends React.Component
|
||||||
|
@ -38,12 +20,9 @@ class AccountSidebar extends React.Component
|
||||||
componentDidMount: =>
|
componentDidMount: =>
|
||||||
@unsubscribers = []
|
@unsubscribers = []
|
||||||
@unsubscribers.push AccountSidebarStore.listen @_onStoreChange
|
@unsubscribers.push AccountSidebarStore.listen @_onStoreChange
|
||||||
@unsubscribers.push ThreadCountsStore.listen @_onStoreChange
|
|
||||||
@configSubscription = NylasEnv.config.observe('core.workspace.showUnreadForAllCategories', @_onStoreChange)
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
componentWillUnmount: =>
|
||||||
unsubscribe() for unsubscribe in @unsubscribers
|
unsubscribe() for unsubscribe in @unsubscribers
|
||||||
@configSubscription?.dispose()
|
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
<ScrollRegion style={flex:1} id="account-sidebar">
|
<ScrollRegion style={flex:1} id="account-sidebar">
|
||||||
|
@ -54,67 +33,12 @@ class AccountSidebar extends React.Component
|
||||||
|
|
||||||
_sections: =>
|
_sections: =>
|
||||||
@state.sections.map (section) =>
|
@state.sections.map (section) =>
|
||||||
<section key={section.label}>
|
<AccountSidebarSection
|
||||||
<div className="heading">{section.label}</div>
|
key={section.label}
|
||||||
{@_itemComponents(section.items)}
|
section={section}
|
||||||
</section>
|
collapsed={@state.collapsed}
|
||||||
|
selected={@state.selected}
|
||||||
_itemComponents: (items) =>
|
onToggleCollapsed={@_onToggleCollapsed} />
|
||||||
components = []
|
|
||||||
|
|
||||||
items.forEach (item) =>
|
|
||||||
components.push(
|
|
||||||
<span key={item.id} className="item-container">
|
|
||||||
<DisclosureTriangle
|
|
||||||
collapsed={@state.collapsed[item.id]}
|
|
||||||
visible={item.children.length > 0}
|
|
||||||
onToggleCollapsed={ => @_onToggleCollapsed(item.id)}/>
|
|
||||||
{@_itemComponent(item)}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
|
|
||||||
if item.children.length and not @state.collapsed[item.id]
|
|
||||||
components.push(
|
|
||||||
<section key={"#{item.id}-children"}>
|
|
||||||
{@_itemComponents(item.children)}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
|
|
||||||
components
|
|
||||||
|
|
||||||
_itemUnreadCount: (item) =>
|
|
||||||
category = item.mailViewFilter.category
|
|
||||||
if category and (category.name is 'inbox' or @state.unreadCountsForAll)
|
|
||||||
return @state.unreadCounts[category.id]
|
|
||||||
return 0
|
|
||||||
|
|
||||||
_itemComponent: (item) =>
|
|
||||||
unless item instanceof WorkspaceStore.SidebarItem
|
|
||||||
throw new Error("AccountSidebar:_itemComponents: sections contained an \
|
|
||||||
item which was not a SidebarItem")
|
|
||||||
|
|
||||||
if item.component
|
|
||||||
Component = item.component
|
|
||||||
<Component
|
|
||||||
item={item}
|
|
||||||
select={item.id is @state.selected?.id } />
|
|
||||||
|
|
||||||
else if item.mailViewFilter
|
|
||||||
<AccountSidebarMailViewItem
|
|
||||||
item={item}
|
|
||||||
itemUnreadCount={@_itemUnreadCount(item)}
|
|
||||||
mailView={item.mailViewFilter}
|
|
||||||
select={item.mailViewFilter.isEqual(@state.selected)} />
|
|
||||||
|
|
||||||
else if item.sheet
|
|
||||||
<SidebarSheetItem
|
|
||||||
item={item}
|
|
||||||
select={item.sheet.id is @state.selected?.id} />
|
|
||||||
|
|
||||||
else
|
|
||||||
throw new Error("AccountSidebar:_itemComponents: each item must have a \
|
|
||||||
custom component, or a sheet or mailViewFilter")
|
|
||||||
|
|
||||||
_onStoreChange: =>
|
_onStoreChange: =>
|
||||||
@setState @_getStateFromStores()
|
@setState @_getStateFromStores()
|
||||||
|
|
||||||
|
@ -127,8 +51,5 @@ class AccountSidebar extends React.Component
|
||||||
_getStateFromStores: =>
|
_getStateFromStores: =>
|
||||||
sections: AccountSidebarStore.sections()
|
sections: AccountSidebarStore.sections()
|
||||||
selected: AccountSidebarStore.selected()
|
selected: AccountSidebarStore.selected()
|
||||||
unreadCounts: ThreadCountsStore.unreadCounts()
|
|
||||||
unreadCountsForAll: NylasEnv.config.get('core.workspace.showUnreadForAllCategories')
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = AccountSidebar
|
module.exports = AccountSidebar
|
||||||
|
|
|
@ -115,6 +115,7 @@ describe "AccountSidebarStore", ->
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Folders',
|
label: 'Folders',
|
||||||
|
iconName: 'folder.png',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'a',
|
id: 'a',
|
||||||
|
|
|
@ -23,6 +23,27 @@
|
||||||
padding-left:@padding-small-horizontal;
|
padding-left:@padding-small-horizontal;
|
||||||
padding-top:@padding-small-horizontal;
|
padding-top:@padding-small-horizontal;
|
||||||
letter-spacing: -0.2px;
|
letter-spacing: -0.2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-button {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: @padding-small-horizontal;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-container {
|
||||||
|
padding-left: @padding-small-horizontal;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.add-item-input {
|
||||||
|
order: 2;
|
||||||
|
font-size: @font-size-small;
|
||||||
|
margin-left: @padding-small-horizontal * 0.85;
|
||||||
|
height: 22px;
|
||||||
|
text-indent: 4px;
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-container {
|
.item-container {
|
||||||
|
@ -112,6 +133,9 @@
|
||||||
color: @source-list-active-color;
|
color: @source-list-active-color;
|
||||||
img.content-mask { background-color: @source-list-active-color; }
|
img.content-mask { background-color: @source-list-active-color; }
|
||||||
}
|
}
|
||||||
|
&.deleted {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
106
spec/tasks/destroy-category-task-spec.coffee
Normal file
106
spec/tasks/destroy-category-task-spec.coffee
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
DestroyCategoryTask = require "../../src/flux/tasks/destroy-category-task"
|
||||||
|
NylasAPI = require "../../src/flux/nylas-api"
|
||||||
|
Task = require '../../src/flux/tasks/task'
|
||||||
|
{APIError} = require '../../src/flux/errors'
|
||||||
|
{Label, Folder, DatabaseStore} = require "nylas-exports"
|
||||||
|
|
||||||
|
describe "DestroyCategoryTask", ->
|
||||||
|
pathOf = (fn) ->
|
||||||
|
fn.calls[0].args[0].path
|
||||||
|
|
||||||
|
methodOf = (fn) ->
|
||||||
|
fn.calls[0].args[0].method
|
||||||
|
|
||||||
|
accountIdOf = (fn) ->
|
||||||
|
fn.calls[0].args[0].accountId
|
||||||
|
|
||||||
|
nameOf = (fn) ->
|
||||||
|
fn.calls[0].args[0].body.display_name
|
||||||
|
|
||||||
|
makeTask = (CategoryClass) ->
|
||||||
|
category = new CategoryClass
|
||||||
|
displayName: "important emails"
|
||||||
|
accountId: "account 123"
|
||||||
|
serverId: "server-444"
|
||||||
|
new DestroyCategoryTask
|
||||||
|
category: category
|
||||||
|
|
||||||
|
describe "performLocal", ->
|
||||||
|
beforeEach ->
|
||||||
|
spyOn(DatabaseStore, 'persistModel')
|
||||||
|
|
||||||
|
it "sets an is deleted flag and persists the category", ->
|
||||||
|
task = makeTask(Folder)
|
||||||
|
task.performLocal()
|
||||||
|
|
||||||
|
expect(DatabaseStore.persistModel).toHaveBeenCalled()
|
||||||
|
model = DatabaseStore.persistModel.calls[0].args[0]
|
||||||
|
expect(model.serverId).toEqual "server-444"
|
||||||
|
expect(model.isDeleted).toBe true
|
||||||
|
|
||||||
|
describe "performRemote", ->
|
||||||
|
it "throws error when no category present", ->
|
||||||
|
waitsForPromise ->
|
||||||
|
task = makeTask(Label)
|
||||||
|
task.category = null
|
||||||
|
task.performRemote()
|
||||||
|
.then ->
|
||||||
|
throw new Error('The promise should reject')
|
||||||
|
.catch Error, (err) ->
|
||||||
|
expect(err).toBeDefined()
|
||||||
|
|
||||||
|
it "throws error when category does not have a serverId", ->
|
||||||
|
waitsForPromise ->
|
||||||
|
task = makeTask(Label)
|
||||||
|
task.category.serverId = undefined
|
||||||
|
task.performRemote()
|
||||||
|
.then ->
|
||||||
|
throw new Error('The promise should reject')
|
||||||
|
.catch Error, (err) ->
|
||||||
|
expect(err).toBeDefined()
|
||||||
|
|
||||||
|
describe "when request succeeds", ->
|
||||||
|
beforeEach ->
|
||||||
|
spyOn(NylasAPI, "makeRequest").andCallFake -> Promise.resolve("null")
|
||||||
|
|
||||||
|
it "sends API req to /labels if user uses labels", ->
|
||||||
|
task = makeTask(Label)
|
||||||
|
task.performRemote()
|
||||||
|
expect(pathOf(NylasAPI.makeRequest)).toBe "/labels/server-444"
|
||||||
|
|
||||||
|
it "sends API req to /folders if user uses folders", ->
|
||||||
|
task = makeTask(Folder)
|
||||||
|
task.performRemote()
|
||||||
|
expect(pathOf(NylasAPI.makeRequest)).toBe "/folders/server-444"
|
||||||
|
|
||||||
|
it "sends DELETE request", ->
|
||||||
|
task = makeTask(Folder)
|
||||||
|
task.performRemote()
|
||||||
|
expect(methodOf(NylasAPI.makeRequest)).toBe "DELETE"
|
||||||
|
|
||||||
|
it "sends the account id", ->
|
||||||
|
task = makeTask(Label)
|
||||||
|
task.performRemote()
|
||||||
|
expect(accountIdOf(NylasAPI.makeRequest)).toBe "account 123"
|
||||||
|
|
||||||
|
describe "when request fails", ->
|
||||||
|
beforeEach ->
|
||||||
|
spyOn(NylasEnv, 'emitError')
|
||||||
|
spyOn(DatabaseStore, 'persistModel').andCallFake ->
|
||||||
|
Promise.resolve()
|
||||||
|
spyOn(NylasAPI, 'makeRequest').andCallFake ->
|
||||||
|
Promise.reject(new APIError({statusCode: 403}))
|
||||||
|
|
||||||
|
it "updates the isDeleted flag for the category and notifies error", ->
|
||||||
|
waitsForPromise ->
|
||||||
|
task = makeTask(Folder)
|
||||||
|
spyOn(task, "_notifyUserOfError")
|
||||||
|
|
||||||
|
task.performRemote().then (status) ->
|
||||||
|
expect(status).toEqual Task.Status.Failed
|
||||||
|
expect(task._notifyUserOfError).toHaveBeenCalled()
|
||||||
|
expect(NylasEnv.emitError).toHaveBeenCalled()
|
||||||
|
expect(DatabaseStore.persistModel).toHaveBeenCalled()
|
||||||
|
model = DatabaseStore.persistModel.calls[0].args[0]
|
||||||
|
expect(model.serverId).toEqual "server-444"
|
||||||
|
expect(model.isDeleted).toBe false
|
17
src/components/disclosure-triangle.cjsx
Normal file
17
src/components/disclosure-triangle.cjsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
React = require 'react'
|
||||||
|
|
||||||
|
class DisclosureTriangle extends React.Component
|
||||||
|
@displayName: 'DisclosureTriangle'
|
||||||
|
|
||||||
|
@propTypes:
|
||||||
|
collapsed: React.PropTypes.bool
|
||||||
|
visible: React.PropTypes.bool
|
||||||
|
onToggleCollapsed: React.PropTypes.func.isRequired
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
classnames = "disclosure-triangle"
|
||||||
|
classnames += " visible" if @props.visible
|
||||||
|
classnames += " collapsed" if @props.collapsed
|
||||||
|
<div className={classnames} onClick={@props.onToggleCollapsed}><div></div></div>
|
||||||
|
|
||||||
|
module.exports = DisclosureTriangle
|
|
@ -32,9 +32,9 @@ class Category extends Model
|
||||||
modelKey: 'displayName'
|
modelKey: 'displayName'
|
||||||
jsonKey: 'display_name'
|
jsonKey: 'display_name'
|
||||||
|
|
||||||
'unread': Attributes.Number
|
'isDeleted': Attributes.Boolean
|
||||||
queryable: true
|
modelKey: 'isDeleted'
|
||||||
modelKey: 'unread'
|
jsonKey: 'is_deleted'
|
||||||
|
|
||||||
hue: ->
|
hue: ->
|
||||||
return 0 unless @displayName
|
return 0 unless @displayName
|
||||||
|
|
|
@ -10,7 +10,7 @@ async = require 'async'
|
||||||
# A 0 code is when an error returns without a status code. These are
|
# A 0 code is when an error returns without a status code. These are
|
||||||
# things like "ESOCKETTIMEDOUT"
|
# things like "ESOCKETTIMEDOUT"
|
||||||
TimeoutErrorCode = 0
|
TimeoutErrorCode = 0
|
||||||
PermanentErrorCodes = [400, 404, 500]
|
PermanentErrorCodes = [400, 403, 404, 500]
|
||||||
CancelledErrorCode = -123
|
CancelledErrorCode = -123
|
||||||
SampleTemporaryErrorCode = 504
|
SampleTemporaryErrorCode = 504
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,18 @@ class CategoryStore extends NylasStore
|
||||||
else
|
else
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
categoryIconName: ->
|
||||||
|
account = AccountStore.current()
|
||||||
|
return "folder.png" unless account
|
||||||
|
|
||||||
|
if account.usesFolders()
|
||||||
|
return "folder.png"
|
||||||
|
else if account.usesLabels()
|
||||||
|
return "tag.png"
|
||||||
|
else
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
# Public: Returns {Folder} or {Label}, depending on the current provider.
|
# Public: Returns {Folder} or {Label}, depending on the current provider.
|
||||||
#
|
#
|
||||||
categoryClass: ->
|
categoryClass: ->
|
||||||
|
|
|
@ -533,14 +533,7 @@ class DraftStore
|
||||||
# don't delay the modal may come up in a state where the draft looks
|
# don't delay the modal may come up in a state where the draft looks
|
||||||
# like it hasn't been restored or has been lost.
|
# like it hasn't been restored or has been lost.
|
||||||
_.delay ->
|
_.delay ->
|
||||||
remote = require('remote')
|
NylasEnv.showErrorDialog(errorMessage)
|
||||||
dialog = remote.require('dialog')
|
|
||||||
dialog.showMessageBox remote.getCurrentWindow(), {
|
|
||||||
type: 'warning'
|
|
||||||
buttons: ['Okay'],
|
|
||||||
message: "Error"
|
|
||||||
detail: errorMessage
|
|
||||||
}
|
|
||||||
, 100
|
, 100
|
||||||
|
|
||||||
module.exports = new DraftStore()
|
module.exports = new DraftStore()
|
||||||
|
|
|
@ -10,7 +10,7 @@ Location = {}
|
||||||
SidebarItems = {}
|
SidebarItems = {}
|
||||||
|
|
||||||
class WorkspaceSidebarItem
|
class WorkspaceSidebarItem
|
||||||
constructor: ({@id, @component, @icon, @name, @sheet, @mailViewFilter, @section, @children}) ->
|
constructor: ({@id, @component, @icon, @name, @sheet, @mailViewFilter, @section, @children, @unreadCount}) ->
|
||||||
if not @sheet and not @mailViewFilter and not @component
|
if not @sheet and not @mailViewFilter and not @component
|
||||||
throw new Error("WorkspaceSidebarItem: You must provide either a sheet \
|
throw new Error("WorkspaceSidebarItem: You must provide either a sheet \
|
||||||
component, or a mailViewFilter for the sidebar item named #{@name}")
|
component, or a mailViewFilter for the sidebar item named #{@name}")
|
||||||
|
|
79
src/flux/tasks/destroy-category-task.coffee
Normal file
79
src/flux/tasks/destroy-category-task.coffee
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
DatabaseStore = require '../stores/database-store'
|
||||||
|
Label = require '../models/label'
|
||||||
|
Folder = require '../models/folder'
|
||||||
|
Task = require './task'
|
||||||
|
ChangeFolderTask = require './change-folder-task'
|
||||||
|
ChangeLabelTask = require './change-labels-task'
|
||||||
|
SyncbackCategoryTask = require './syncback-category-task'
|
||||||
|
NylasAPI = require '../nylas-api'
|
||||||
|
{APIError} = require '../errors'
|
||||||
|
|
||||||
|
class DestroyCategoryTask extends Task
|
||||||
|
|
||||||
|
constructor: ({@category}={}) ->
|
||||||
|
super
|
||||||
|
|
||||||
|
label: ->
|
||||||
|
name = @category.displayName
|
||||||
|
if @category instanceof Label
|
||||||
|
"Deleting label #{name}..."
|
||||||
|
else
|
||||||
|
"Deleting folder #{name}..."
|
||||||
|
|
||||||
|
isDependentTask: (other) ->
|
||||||
|
(other instanceof ChangeFolderTask) or
|
||||||
|
(other instanceof ChangeLabelTask) or
|
||||||
|
(other instanceof SyncbackCategoryTask)
|
||||||
|
|
||||||
|
performLocal: ->
|
||||||
|
if not @category
|
||||||
|
return Promise.reject(new Error("Attempt to call DestroyCategoryTask.performLocal without @category."))
|
||||||
|
@category.isDeleted = true
|
||||||
|
DatabaseStore.persistModel @category
|
||||||
|
|
||||||
|
performRemote: ->
|
||||||
|
if not @category
|
||||||
|
return Promise.reject(new Error("Attempt to call DestroyCategoryTask.performRemote without @category."))
|
||||||
|
|
||||||
|
if not @category.serverId
|
||||||
|
return Promise.reject(new Error("Attempt to call DestroyCategoryTask.performRemote without @category.serverId."))
|
||||||
|
|
||||||
|
if @category instanceof Label
|
||||||
|
path = "/labels/#{@category.serverId}"
|
||||||
|
else
|
||||||
|
path = "/folders/#{@category.serverId}"
|
||||||
|
|
||||||
|
NylasAPI.makeRequest
|
||||||
|
path: path
|
||||||
|
method: 'DELETE'
|
||||||
|
accountId: @category.accountId
|
||||||
|
returnsModel: false
|
||||||
|
.then ->
|
||||||
|
return Promise.resolve(Task.Status.Success)
|
||||||
|
.catch APIError, (err) =>
|
||||||
|
if err.statusCode in NylasAPI.PermanentErrorCodes
|
||||||
|
# Revert isDeleted flag
|
||||||
|
@category.isDeleted = false
|
||||||
|
DatabaseStore.persistModel(@category).then =>
|
||||||
|
NylasEnv.emitError(
|
||||||
|
new Error("Deleting category responded with #{err.statusCode}!")
|
||||||
|
)
|
||||||
|
@_notifyUserOfError()
|
||||||
|
return Promise.resolve(Task.Status.Failed)
|
||||||
|
else
|
||||||
|
return Promise.resolve(Task.Status.Retry)
|
||||||
|
|
||||||
|
_notifyUserOfError: (category = @category) ->
|
||||||
|
displayName = category.displayName
|
||||||
|
label = if category instanceof Label
|
||||||
|
'label'
|
||||||
|
else
|
||||||
|
'folder'
|
||||||
|
|
||||||
|
msg = "The #{label} #{displayName} could not be deleted."
|
||||||
|
if label is 'folder'
|
||||||
|
msg += " Make sure the folder you want to delete is empty before deleting it."
|
||||||
|
|
||||||
|
NylasEnv.showErrorDialog(msg)
|
||||||
|
|
||||||
|
module.exports = DestroyCategoryTask
|
|
@ -30,6 +30,7 @@ class NylasComponentKit
|
||||||
@load "InjectedComponentSet", 'injected-component-set'
|
@load "InjectedComponentSet", 'injected-component-set'
|
||||||
@load "TimeoutTransitionGroup", 'timeout-transition-group'
|
@load "TimeoutTransitionGroup", 'timeout-transition-group'
|
||||||
@load "ConfigPropContainer", "config-prop-container"
|
@load "ConfigPropContainer", "config-prop-container"
|
||||||
|
@load "DisclosureTriangle", "disclosure-triangle"
|
||||||
|
|
||||||
@load "ScrollRegion", 'scroll-region'
|
@load "ScrollRegion", 'scroll-region'
|
||||||
@load "ResizableRegion", 'resizable-region'
|
@load "ResizableRegion", 'resizable-region'
|
||||||
|
|
|
@ -86,6 +86,7 @@ class NylasExports
|
||||||
@require "ChangeLabelsTask", 'flux/tasks/change-labels-task'
|
@require "ChangeLabelsTask", 'flux/tasks/change-labels-task'
|
||||||
@require "ChangeFolderTask", 'flux/tasks/change-folder-task'
|
@require "ChangeFolderTask", 'flux/tasks/change-folder-task'
|
||||||
@require "SyncbackCategoryTask", 'flux/tasks/syncback-category-task'
|
@require "SyncbackCategoryTask", 'flux/tasks/syncback-category-task'
|
||||||
|
@require "DestroyCategoryTask", 'flux/tasks/destroy-category-task'
|
||||||
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
|
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
|
||||||
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft'
|
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft'
|
||||||
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
|
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
|
||||||
|
|
|
@ -140,10 +140,8 @@ class CategoryMailViewFilter extends MailViewFilter
|
||||||
|
|
||||||
if cat.name
|
if cat.name
|
||||||
@iconName = "#{cat.name}.png"
|
@iconName = "#{cat.name}.png"
|
||||||
else if AccountStore.current().usesLabels()
|
|
||||||
@iconName = "tag.png"
|
|
||||||
else
|
else
|
||||||
@iconName = "folder.png"
|
@iconName = CategoryStore.categoryIconName()
|
||||||
|
|
||||||
@
|
@
|
||||||
|
|
||||||
|
|
|
@ -843,6 +843,15 @@ class NylasEnvConstructor extends Model
|
||||||
dialog = remote.require('dialog')
|
dialog = remote.require('dialog')
|
||||||
dialog.showSaveDialog(@getCurrentWindow(), {title: 'Save File', defaultPath}, callback)
|
dialog.showSaveDialog(@getCurrentWindow(), {title: 'Save File', defaultPath}, callback)
|
||||||
|
|
||||||
|
showErrorDialog: (message) ->
|
||||||
|
dialog = remote.require('dialog')
|
||||||
|
dialog.showMessageBox null, {
|
||||||
|
type: 'warning'
|
||||||
|
buttons: ['Okay'],
|
||||||
|
message: "Error"
|
||||||
|
detail: message
|
||||||
|
}
|
||||||
|
|
||||||
saveSync: ->
|
saveSync: ->
|
||||||
stateString = JSON.stringify(@savedState)
|
stateString = JSON.stringify(@savedState)
|
||||||
if statePath = @constructor.getStatePath()
|
if statePath = @constructor.getStatePath()
|
||||||
|
|
Loading…
Reference in a new issue