mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-28 11:24:11 +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'
|
||||
{Actions} = require("nylas-exports")
|
||||
React = require 'react'
|
||||
{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
|
||||
@displayName: "AccountSidebarItem"
|
||||
|
||||
render: =>
|
||||
className = "item " + if @props.select then " selected" else ""
|
||||
<div className={className} onClick={@_onClick} id={@props.item.id}>
|
||||
<div className="name">{@props.item.displayName}</div>
|
||||
</div>
|
||||
@propTypes: {
|
||||
item: React.PropTypes.object.isRequired
|
||||
selected: React.PropTypes.object.isRequired
|
||||
onToggleCollapsed: React.PropTypes.func.isRequired
|
||||
collapsed: React.PropTypes.bool
|
||||
onDestroyItem: React.PropTypes.func
|
||||
}
|
||||
|
||||
_onClick: (event) =>
|
||||
event.preventDefault()
|
||||
Actions.selectView(@props.item.view)
|
||||
@defaultProps: {
|
||||
collapsed: false
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -2,7 +2,6 @@ React = require 'react'
|
|||
classNames = require 'classnames'
|
||||
{Actions,
|
||||
Utils,
|
||||
ThreadCountsStore,
|
||||
WorkspaceStore,
|
||||
AccountStore,
|
||||
FocusedMailViewStore,
|
||||
|
@ -17,7 +16,6 @@ class AccountSidebarMailViewItem extends React.Component
|
|||
@propTypes:
|
||||
select: React.PropTypes.bool
|
||||
item: React.PropTypes.object.isRequired
|
||||
itemUnreadCount: React.PropTypes.number
|
||||
mailView: React.PropTypes.object.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
|
@ -27,10 +25,13 @@ class AccountSidebarMailViewItem extends React.Component
|
|||
!Utils.isEqualReact(@props, nextProps) or !Utils.isEqualReact(@state, nextState)
|
||||
|
||||
render: =>
|
||||
isDeleted = @props.mailView?.category?.isDeleted is true
|
||||
|
||||
containerClass = classNames
|
||||
'item': true
|
||||
'selected': @props.select
|
||||
'dropping': @state.isDropping
|
||||
'deleted': isDeleted
|
||||
|
||||
<DropZone className={containerClass}
|
||||
onClick={@_onClick}
|
||||
|
@ -44,10 +45,10 @@ class AccountSidebarMailViewItem extends React.Component
|
|||
</DropZone>
|
||||
|
||||
_renderUnreadCount: =>
|
||||
return false if @props.itemUnreadCount is 0
|
||||
return false unless @props.item.unreadCount
|
||||
className = 'item-count-box '
|
||||
className += @props.mailView.category?.name
|
||||
<div className={className}>{@props.itemUnreadCount}</div>
|
||||
<div className={className}>{@props.item.unreadCount}</div>
|
||||
|
||||
_renderIcon: ->
|
||||
<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,
|
||||
CategoryStore,
|
||||
AccountStore,
|
||||
ThreadCountsStore,
|
||||
WorkspaceStore,
|
||||
DraftCountStore,
|
||||
Actions,
|
||||
|
@ -12,7 +13,8 @@ _ = require 'underscore'
|
|||
Message,
|
||||
MailViewFilter,
|
||||
FocusedMailViewStore,
|
||||
NylasAPI,
|
||||
SyncbackCategoryTask,
|
||||
DestroyCategoryTask,
|
||||
Thread} = require 'nylas-exports'
|
||||
|
||||
class AccountSidebarStore extends NylasStore
|
||||
|
@ -38,7 +40,9 @@ class AccountSidebarStore extends NylasStore
|
|||
@listenTo CategoryStore, @_refreshSections
|
||||
@listenTo WorkspaceStore, @_refreshSections
|
||||
@listenTo DraftCountStore, @_refreshSections
|
||||
@listenTo ThreadCountsStore, @_refreshSections
|
||||
@listenTo FocusedMailViewStore, => @trigger()
|
||||
@configSubscription = NylasEnv.config.observe('core.workspace.showUnreadForAllCategories', @_refreshSections)
|
||||
|
||||
_refreshSections: =>
|
||||
account = AccountStore.current()
|
||||
|
@ -111,6 +115,9 @@ class AccountSidebarStore extends NylasStore
|
|||
@_sections.push
|
||||
label: CategoryStore.categoryLabel()
|
||||
items: userCategoryItemsHierarchical
|
||||
iconName: CategoryStore.categoryIconName()
|
||||
createItem: @_createCategory
|
||||
destroyItem: @_destroyCategory
|
||||
|
||||
@trigger()
|
||||
|
||||
|
@ -125,5 +132,23 @@ class AccountSidebarStore extends NylasStore
|
|||
id: category.id,
|
||||
name: shortenedName || category.displayName
|
||||
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()
|
||||
|
|
|
@ -1,26 +1,8 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
{Actions, MailViewFilter, WorkspaceStore, ThreadCountsStore} = require("nylas-exports")
|
||||
{ScrollRegion, Flexbox} = require("nylas-component-kit")
|
||||
SidebarDividerItem = require("./account-sidebar-divider-item")
|
||||
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>
|
||||
{ScrollRegion} = require 'nylas-component-kit'
|
||||
AccountSidebarStore = require './account-sidebar-store'
|
||||
AccountSidebarSection = require './account-sidebar-section'
|
||||
|
||||
|
||||
class AccountSidebar extends React.Component
|
||||
|
@ -38,12 +20,9 @@ class AccountSidebar extends React.Component
|
|||
componentDidMount: =>
|
||||
@unsubscribers = []
|
||||
@unsubscribers.push AccountSidebarStore.listen @_onStoreChange
|
||||
@unsubscribers.push ThreadCountsStore.listen @_onStoreChange
|
||||
@configSubscription = NylasEnv.config.observe('core.workspace.showUnreadForAllCategories', @_onStoreChange)
|
||||
|
||||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
@configSubscription?.dispose()
|
||||
|
||||
render: =>
|
||||
<ScrollRegion style={flex:1} id="account-sidebar">
|
||||
|
@ -54,67 +33,12 @@ class AccountSidebar extends React.Component
|
|||
|
||||
_sections: =>
|
||||
@state.sections.map (section) =>
|
||||
<section key={section.label}>
|
||||
<div className="heading">{section.label}</div>
|
||||
{@_itemComponents(section.items)}
|
||||
</section>
|
||||
|
||||
_itemComponents: (items) =>
|
||||
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")
|
||||
|
||||
<AccountSidebarSection
|
||||
key={section.label}
|
||||
section={section}
|
||||
collapsed={@state.collapsed}
|
||||
selected={@state.selected}
|
||||
onToggleCollapsed={@_onToggleCollapsed} />
|
||||
_onStoreChange: =>
|
||||
@setState @_getStateFromStores()
|
||||
|
||||
|
@ -127,8 +51,5 @@ class AccountSidebar extends React.Component
|
|||
_getStateFromStores: =>
|
||||
sections: AccountSidebarStore.sections()
|
||||
selected: AccountSidebarStore.selected()
|
||||
unreadCounts: ThreadCountsStore.unreadCounts()
|
||||
unreadCountsForAll: NylasEnv.config.get('core.workspace.showUnreadForAllCategories')
|
||||
|
||||
|
||||
module.exports = AccountSidebar
|
||||
|
|
|
@ -115,6 +115,7 @@ describe "AccountSidebarStore", ->
|
|||
},
|
||||
{
|
||||
label: 'Folders',
|
||||
iconName: 'folder.png',
|
||||
items: [
|
||||
{
|
||||
id: 'a',
|
||||
|
|
|
@ -23,6 +23,27 @@
|
|||
padding-left:@padding-small-horizontal;
|
||||
padding-top:@padding-small-horizontal;
|
||||
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 {
|
||||
|
@ -112,6 +133,9 @@
|
|||
color: @source-list-active-color;
|
||||
img.content-mask { background-color: @source-list-active-color; }
|
||||
}
|
||||
&.deleted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
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'
|
||||
jsonKey: 'display_name'
|
||||
|
||||
'unread': Attributes.Number
|
||||
queryable: true
|
||||
modelKey: 'unread'
|
||||
'isDeleted': Attributes.Boolean
|
||||
modelKey: 'isDeleted'
|
||||
jsonKey: 'is_deleted'
|
||||
|
||||
hue: ->
|
||||
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
|
||||
# things like "ESOCKETTIMEDOUT"
|
||||
TimeoutErrorCode = 0
|
||||
PermanentErrorCodes = [400, 404, 500]
|
||||
PermanentErrorCodes = [400, 403, 404, 500]
|
||||
CancelledErrorCode = -123
|
||||
SampleTemporaryErrorCode = 504
|
||||
|
||||
|
|
|
@ -61,6 +61,18 @@ class CategoryStore extends NylasStore
|
|||
else
|
||||
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.
|
||||
#
|
||||
categoryClass: ->
|
||||
|
|
|
@ -533,14 +533,7 @@ class DraftStore
|
|||
# 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.
|
||||
_.delay ->
|
||||
remote = require('remote')
|
||||
dialog = remote.require('dialog')
|
||||
dialog.showMessageBox remote.getCurrentWindow(), {
|
||||
type: 'warning'
|
||||
buttons: ['Okay'],
|
||||
message: "Error"
|
||||
detail: errorMessage
|
||||
}
|
||||
NylasEnv.showErrorDialog(errorMessage)
|
||||
, 100
|
||||
|
||||
module.exports = new DraftStore()
|
||||
|
|
|
@ -10,7 +10,7 @@ Location = {}
|
|||
SidebarItems = {}
|
||||
|
||||
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
|
||||
throw new Error("WorkspaceSidebarItem: You must provide either a sheet \
|
||||
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 "TimeoutTransitionGroup", 'timeout-transition-group'
|
||||
@load "ConfigPropContainer", "config-prop-container"
|
||||
@load "DisclosureTriangle", "disclosure-triangle"
|
||||
|
||||
@load "ScrollRegion", 'scroll-region'
|
||||
@load "ResizableRegion", 'resizable-region'
|
||||
|
|
|
@ -86,6 +86,7 @@ class NylasExports
|
|||
@require "ChangeLabelsTask", 'flux/tasks/change-labels-task'
|
||||
@require "ChangeFolderTask", 'flux/tasks/change-folder-task'
|
||||
@require "SyncbackCategoryTask", 'flux/tasks/syncback-category-task'
|
||||
@require "DestroyCategoryTask", 'flux/tasks/destroy-category-task'
|
||||
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
|
||||
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft'
|
||||
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
|
||||
|
|
|
@ -140,10 +140,8 @@ class CategoryMailViewFilter extends MailViewFilter
|
|||
|
||||
if cat.name
|
||||
@iconName = "#{cat.name}.png"
|
||||
else if AccountStore.current().usesLabels()
|
||||
@iconName = "tag.png"
|
||||
else
|
||||
@iconName = "folder.png"
|
||||
@iconName = CategoryStore.categoryIconName()
|
||||
|
||||
@
|
||||
|
||||
|
|
|
@ -843,6 +843,15 @@ class NylasEnvConstructor extends Model
|
|||
dialog = remote.require('dialog')
|
||||
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: ->
|
||||
stateString = JSON.stringify(@savedState)
|
||||
if statePath = @constructor.getStatePath()
|
||||
|
|
Loading…
Reference in a new issue