mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-07 05:04:58 +08:00
fix(thread-list): Narrow mode, and new selection rules for three-pane
Summary: Fix bug in apm_wrapper Refactor model selection so that it's easier to understand the logic for split vs list mode Test Plan: Run new specs (WIP) Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1619
This commit is contained in:
parent
d7f12873b3
commit
aa10ddfd1c
20 changed files with 590 additions and 154 deletions
|
@ -25,7 +25,7 @@ class APMWrapper
|
|||
args.push('--no-color')
|
||||
new BufferedProcess({command, args, stdout, stderr, exit, options})
|
||||
|
||||
runCommandReturningPackages: (args, callback) ->
|
||||
runCommandReturningPackages: (args, errorMessage, callback) ->
|
||||
@runCommand args, (code, stdout, stderr) ->
|
||||
if code is 0
|
||||
try
|
||||
|
@ -43,7 +43,7 @@ class APMWrapper
|
|||
loadInstalled: (callback) ->
|
||||
args = ['ls', '--json']
|
||||
errorMessage = 'Fetching local packages failed.'
|
||||
apmProcess = @runCommandReturningPackages(args, callback)
|
||||
apmProcess = @runCommandReturningPackages(args, errorMessage, callback)
|
||||
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
|
@ -54,7 +54,7 @@ class APMWrapper
|
|||
args.push('--compatible', version) if semver.valid(version)
|
||||
errorMessage = 'Fetching featured packages failed.'
|
||||
|
||||
apmProcess = @runCommandReturningPackages(args, callback)
|
||||
apmProcess = @runCommandReturningPackages(args, errorMessage, callback)
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
loadOutdated: (callback) ->
|
||||
|
@ -63,21 +63,21 @@ class APMWrapper
|
|||
args.push('--compatible', version) if semver.valid(version)
|
||||
errorMessage = 'Fetching outdated packages and themes failed.'
|
||||
|
||||
apmProcess = @runCommandReturningPackages(args, callback)
|
||||
apmProcess = @runCommandReturningPackages(args, errorMessage, callback)
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
loadPackage: (packageName, callback) ->
|
||||
args = ['view', packageName, '--json']
|
||||
errorMessage = "Fetching package '#{packageName}' failed."
|
||||
|
||||
apmProcess = @runCommandReturningPackages(args, callback)
|
||||
apmProcess = @runCommandReturningPackages(args, errorMessage, callback)
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
loadCompatiblePackageVersion: (packageName, callback) ->
|
||||
args = ['view', packageName, '--json', '--compatible', @normalizeVersion(atom.getVersion())]
|
||||
errorMessage = "Fetching package '#{packageName}' failed."
|
||||
|
||||
apmProcess = @runCommandReturningPackages(args, callback)
|
||||
apmProcess = @runCommandReturningPackages(args, errorMessage, callback)
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
getInstalled: ->
|
||||
|
|
|
@ -59,6 +59,7 @@ class DraftList extends React.Component
|
|||
commands={@commands}
|
||||
onDoubleClick={@_onDoubleClick}
|
||||
itemPropsProvider={ -> {} }
|
||||
itemHeight={39}
|
||||
className="draft-list"
|
||||
collection="draft" />
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ ThreadListStore = Reflux.createStore
|
|||
@setView new DatabaseView Thread, {matchers}, (item) ->
|
||||
DatabaseStore.findAll(Message, {threadId: item.id})
|
||||
|
||||
Actions.focusInCollection(collection: 'thread', item: null)
|
||||
Actions.setFocus(collection: 'thread', item: null)
|
||||
|
||||
# Inbound Events
|
||||
|
||||
|
@ -128,9 +128,9 @@ ThreadListStore = Reflux.createStore
|
|||
task = new AddRemoveTagsTask(thread, ['archive'], ['inbox'])
|
||||
Actions.queueTask(task)
|
||||
if thread.id is focusedId
|
||||
Actions.focusInCollection(collection: 'thread', item: null)
|
||||
Actions.setFocus(collection: 'thread', item: null)
|
||||
if thread.id is keyboardId
|
||||
Actions.focusKeyboardInCollection(collection: 'thread', item: null)
|
||||
Actions.setCursorPosition(collection: 'thread', item: null)
|
||||
|
||||
@_view.selection.clear()
|
||||
|
||||
|
@ -175,10 +175,13 @@ ThreadListStore = Reflux.createStore
|
|||
nextFocus = null
|
||||
|
||||
@_afterViewUpdate.push ->
|
||||
Actions.focusInCollection(collection: 'thread', item: nextFocus)
|
||||
Actions.focusKeyboardInCollection(collection: 'thread', item: nextKeyboard)
|
||||
Actions.setFocus(collection: 'thread', item: nextFocus)
|
||||
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
|
||||
|
||||
_autofocusForLayoutMode: ->
|
||||
focusedId = FocusedContentStore.focusedId('thread')
|
||||
if WorkspaceStore.layoutMode() is "split" and not focusedId
|
||||
_.defer => Actions.focusInCollection(collection: 'thread', item: @_view.get(0))
|
||||
layoutMode = WorkspaceStore.layoutMode()
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
if layoutMode is 'split' and not focused and @_view.selection.count() is 0
|
||||
item = @_view.get(0)
|
||||
_.defer =>
|
||||
Actions.setFocus({collection: 'thread', item: item})
|
||||
|
|
|
@ -44,6 +44,10 @@ class ThreadList extends React.Component
|
|||
|
||||
@containerRequired: false
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
style: 'unknown'
|
||||
|
||||
componentWillMount: =>
|
||||
labelComponents = (thread) =>
|
||||
for label in @state.threadLabelComponents
|
||||
|
@ -88,7 +92,29 @@ class ThreadList extends React.Component
|
|||
resolver: (thread) =>
|
||||
<span className="timestamp">{timestamp(thread.lastMessageTimestamp)}</span>
|
||||
|
||||
@columns = [c1, c2, c3, c4]
|
||||
@wideColumns = [c1, c2, c3, c4]
|
||||
|
||||
cNarrow = new ListTabular.Column
|
||||
name: "Item"
|
||||
resolver: (thread) =>
|
||||
pencil = []
|
||||
hasDraft = _.find (thread.metadata ? []), (m) -> m.draft
|
||||
if hasDraft
|
||||
pencil = <RetinaImg name="icon-draft-pencil.png" className="draft-icon" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
<div>
|
||||
<div style={display: 'flex'}>
|
||||
<ThreadListIcon thread={thread} />
|
||||
<ThreadListParticipants thread={thread} />
|
||||
<span className="timestamp">{timestamp(thread.lastMessageTimestamp)}</span>
|
||||
{pencil}
|
||||
</div>
|
||||
<div className="subject">{subject(thread.subject)}</div>
|
||||
<div className="snippet">{thread.snippet}</div>
|
||||
</div>
|
||||
|
||||
@narrowColumns = [cNarrow]
|
||||
|
||||
@commands =
|
||||
'core:remove-item': @_onArchive
|
||||
'core:remove-and-previous': -> Actions.archiveAndPrevious()
|
||||
|
@ -100,15 +126,42 @@ class ThreadList extends React.Component
|
|||
className: classNames
|
||||
'unread': item.isUnread()
|
||||
|
||||
componentDidMount: =>
|
||||
window.addEventListener('resize', @_onResize, true)
|
||||
@_onResize()
|
||||
|
||||
componentWillUnmount: =>
|
||||
window.removeEventListener('resize', @_onResize)
|
||||
|
||||
render: =>
|
||||
<MultiselectList
|
||||
dataStore={ThreadListStore}
|
||||
columns={@columns}
|
||||
commands={@commands}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
className="thread-list"
|
||||
scrollTooltipComponent={ThreadListScrollTooltip}
|
||||
collection="thread" />
|
||||
if @state.style is 'wide'
|
||||
<MultiselectList
|
||||
dataStore={ThreadListStore}
|
||||
columns={@wideColumns}
|
||||
commands={@commands}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={39}
|
||||
className="thread-list"
|
||||
scrollTooltipComponent={ThreadListScrollTooltip}
|
||||
collection="thread" />
|
||||
else if @state.style is 'narrow'
|
||||
<MultiselectList
|
||||
dataStore={ThreadListStore}
|
||||
columns={@narrowColumns}
|
||||
commands={@commands}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={90}
|
||||
className="thread-list thread-list-narrow"
|
||||
scrollTooltipComponent={ThreadListScrollTooltip}
|
||||
collection="thread" />
|
||||
else
|
||||
<div></div>
|
||||
|
||||
_onResize: (event) =>
|
||||
current = @state.style
|
||||
desired = if React.findDOMNode(@).offsetWidth < 540 then 'narrow' else 'wide'
|
||||
if current isnt desired
|
||||
@setState(style: desired)
|
||||
|
||||
# Additional Commands
|
||||
|
||||
|
|
|
@ -180,3 +180,33 @@
|
|||
background-image:url(../static/images/thread-list/icon-star-hover-@2x.png);
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.thread-list-narrow {
|
||||
.timestamp {
|
||||
flex: 1;
|
||||
order: 100;
|
||||
}
|
||||
.participants {
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
.thread-icon {
|
||||
margin-right:6px;
|
||||
}
|
||||
.subject {
|
||||
font-size: @font-size-base;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
padding-top:2px;
|
||||
padding-bottom:2px;
|
||||
margin-left:30px;
|
||||
}
|
||||
.snippet {
|
||||
font-size: @font-size-small;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.7;
|
||||
text-align: left;
|
||||
margin-left:30px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ module.exports =
|
|||
notif.onclick = ->
|
||||
atom.displayWindow()
|
||||
Actions.focusTag(new Tag(name: "inbox", id: "inbox"))
|
||||
Actions.focusInCollection(collection: 'thread', item: threads[msg.threadId])
|
||||
Actions.setFocus(collection: 'thread', item: threads[msg.threadId])
|
||||
|
||||
if newUnreadInInbox.length > 1
|
||||
new Notification("#{newUnreadInInbox.length} Unread Messages", {
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
MultiselectListInteractionHandler = require '../../src/components/multiselect-list-interaction-handler'
|
||||
WorkspaceStore = require '../../src/flux/stores/workspace-store'
|
||||
FocusedContentStore = require '../../src/flux/stores/focused-content-store'
|
||||
Thread = require '../../src/flux/models/thread'
|
||||
Actions = require '../../src/flux/actions'
|
||||
_ = require 'underscore'
|
||||
|
||||
describe "MultiselectListInteractionHandler", ->
|
||||
beforeEach ->
|
||||
@item = new Thread(id:'123')
|
||||
@itemFocus = new Thread({id: 'focus'})
|
||||
@itemKeyboardFocus = new Thread({id: 'keyboard-focus'})
|
||||
@itemAfterFocus = new Thread(id:'after-focus')
|
||||
@itemAfterKeyboardFocus = new Thread(id:'after-keyboard-focus')
|
||||
|
||||
data = [@item, @itemFocus, @itemAfterFocus, @itemKeyboardFocus, @itemAfterKeyboardFocus]
|
||||
|
||||
@dataView =
|
||||
selection:
|
||||
toggle: jasmine.createSpy('toggle')
|
||||
expandTo: jasmine.createSpy('expandTo')
|
||||
walk: jasmine.createSpy('walk')
|
||||
get: (idx) ->
|
||||
data[idx]
|
||||
getById: (id) ->
|
||||
_.find data, (item) -> item.id is id
|
||||
indexOfId: (id) ->
|
||||
_.findIndex data, (item) -> item.id is id
|
||||
count: -> data.length
|
||||
|
||||
@collection = 'threads'
|
||||
@handler = new MultiselectListInteractionHandler(@dataView, @collection)
|
||||
@isRootSheet = true
|
||||
|
||||
spyOn(WorkspaceStore, 'topSheet').andCallFake => {root: @isRootSheet}
|
||||
spyOn(FocusedContentStore, 'keyboardCursorId').andCallFake -> 'keyboard-focus'
|
||||
spyOn(FocusedContentStore, 'focusedId').andCallFake -> 'focus'
|
||||
spyOn(Actions, 'setFocus')
|
||||
spyOn(Actions, 'setCursorPosition')
|
||||
|
||||
it "should never show focus", ->
|
||||
expect(@handler.shouldShowFocus()).toEqual(false)
|
||||
|
||||
it "should always show the keyboard cursor", ->
|
||||
expect(@handler.shouldShowKeyboardCursor()).toEqual(true)
|
||||
|
||||
describe "onClick", ->
|
||||
it "should focus list items", ->
|
||||
@handler.onClick(@item)
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: @item})
|
||||
|
||||
describe "onMetaClick", ->
|
||||
it "shoud toggle selection", ->
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@dataView.selection.toggle).toHaveBeenCalledWith(@item)
|
||||
|
||||
it "should focus the keyboard on the clicked item", ->
|
||||
@handler.onMetaClick(@item)
|
||||
expect(Actions.setCursorPosition).toHaveBeenCalledWith({collection: @collection, item: @item})
|
||||
|
||||
describe "onShiftClick", ->
|
||||
it "should expand selection", ->
|
||||
@handler.onShiftClick(@item)
|
||||
expect(@dataView.selection.expandTo).toHaveBeenCalledWith(@item)
|
||||
|
||||
it "should focus the keyboard on the clicked item", ->
|
||||
@handler.onShiftClick(@item)
|
||||
expect(Actions.setCursorPosition).toHaveBeenCalledWith({collection: @collection, item: @item})
|
||||
|
||||
describe "onEnter", ->
|
||||
it "should focus the item with the current keyboard selection", ->
|
||||
@handler.onEnter()
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: @itemKeyboardFocus})
|
||||
|
||||
describe "onSelect (x key on keyboard)", ->
|
||||
describe "on the root view", ->
|
||||
it "should toggle the selection of the keyboard item", ->
|
||||
@isRootSheet = true
|
||||
@handler.onSelect()
|
||||
expect(@dataView.selection.toggle).toHaveBeenCalledWith(@itemKeyboardFocus)
|
||||
|
||||
describe "on the thread view", ->
|
||||
it "should toggle the selection of the focused item", ->
|
||||
@isRootSheet = false
|
||||
@handler.onSelect()
|
||||
expect(@dataView.selection.toggle).toHaveBeenCalledWith(@itemFocus)
|
||||
|
||||
describe "onShift", ->
|
||||
describe "on the root view", ->
|
||||
beforeEach ->
|
||||
@isRootSheet = true
|
||||
|
||||
it "should shift the keyboard item", ->
|
||||
@handler.onShift(1, {})
|
||||
expect(Actions.setCursorPosition).toHaveBeenCalledWith({collection: @collection, item: @itemAfterKeyboardFocus})
|
||||
|
||||
it "should walk selection if the select option is passed", ->
|
||||
@handler.onShift(1, select: true)
|
||||
expect(@dataView.selection.walk).toHaveBeenCalledWith({current: @itemKeyboardFocus, next: @itemAfterKeyboardFocus})
|
||||
|
||||
describe "on the thread view", ->
|
||||
beforeEach ->
|
||||
@isRootSheet = false
|
||||
|
||||
it "should shift the focused item", ->
|
||||
@handler.onShift(1, {})
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: @itemAfterFocus})
|
|
@ -0,0 +1,160 @@
|
|||
MultiselectSplitInteractionHandler = require '../../src/components/multiselect-split-interaction-handler'
|
||||
WorkspaceStore = require '../../src/flux/stores/workspace-store'
|
||||
FocusedContentStore = require '../../src/flux/stores/focused-content-store'
|
||||
Thread = require '../../src/flux/models/thread'
|
||||
Actions = require '../../src/flux/actions'
|
||||
_ = require 'underscore'
|
||||
|
||||
describe "MultiselectSplitInteractionHandler", ->
|
||||
beforeEach ->
|
||||
@item = new Thread(id:'123')
|
||||
@itemFocus = new Thread({id: 'focus'})
|
||||
@itemKeyboardFocus = new Thread({id: 'keyboard-focus'})
|
||||
@itemAfterFocus = new Thread(id:'after-focus')
|
||||
@itemAfterKeyboardFocus = new Thread(id:'after-keyboard-focus')
|
||||
|
||||
data = [@item, @itemFocus, @itemAfterFocus, @itemKeyboardFocus, @itemAfterKeyboardFocus]
|
||||
@selection = []
|
||||
@dataView =
|
||||
selection:
|
||||
toggle: jasmine.createSpy('toggle')
|
||||
expandTo: jasmine.createSpy('expandTo')
|
||||
add: jasmine.createSpy('add')
|
||||
walk: jasmine.createSpy('walk')
|
||||
clear: jasmine.createSpy('clear')
|
||||
count: => @selection.length
|
||||
items: => @selection
|
||||
top: => @selection[-1]
|
||||
|
||||
get: (idx) ->
|
||||
data[idx]
|
||||
getById: (id) ->
|
||||
_.find data, (item) -> item.id is id
|
||||
indexOfId: (id) ->
|
||||
_.findIndex data, (item) -> item.id is id
|
||||
count: -> data.length
|
||||
|
||||
@collection = 'threads'
|
||||
@handler = new MultiselectSplitInteractionHandler(@dataView, @collection)
|
||||
@isRootSheet = true
|
||||
|
||||
spyOn(WorkspaceStore, 'topSheet').andCallFake => {root: @isRootSheet}
|
||||
spyOn(Actions, 'setFocus')
|
||||
spyOn(Actions, 'setCursorPosition')
|
||||
|
||||
it "should always show focus", ->
|
||||
expect(@handler.shouldShowFocus()).toEqual(true)
|
||||
|
||||
it "should show the keyboard cursor when multiple items are selected", ->
|
||||
@selection = []
|
||||
expect(@handler.shouldShowKeyboardCursor()).toEqual(false)
|
||||
@selection = [@item]
|
||||
expect(@handler.shouldShowKeyboardCursor()).toEqual(false)
|
||||
@selection = [@item, @itemFocus]
|
||||
expect(@handler.shouldShowKeyboardCursor()).toEqual(true)
|
||||
|
||||
describe "onClick", ->
|
||||
it "should focus list items", ->
|
||||
@handler.onClick(@item)
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: @item})
|
||||
|
||||
describe "onMetaClick", ->
|
||||
describe "when there is currently a focused item", ->
|
||||
beforeEach ->
|
||||
spyOn(FocusedContentStore, 'focused').andCallFake => @itemFocus
|
||||
spyOn(FocusedContentStore, 'focusedId').andCallFake -> 'focus'
|
||||
|
||||
it "should turn the focused item into the first selected item", ->
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@dataView.selection.add).toHaveBeenCalledWith(@itemFocus)
|
||||
|
||||
it "should clear the focus", ->
|
||||
@handler.onMetaClick(@item)
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: null})
|
||||
|
||||
it "should toggle selection", ->
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@dataView.selection.toggle).toHaveBeenCalledWith(@item)
|
||||
|
||||
it "should call _checkSelectionAndFocusConsistency", ->
|
||||
spyOn(@handler, '_checkSelectionAndFocusConsistency')
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()
|
||||
|
||||
describe "onShiftClick", ->
|
||||
describe "when there is currently a focused item", ->
|
||||
beforeEach ->
|
||||
spyOn(FocusedContentStore, 'focused').andCallFake => @itemFocus
|
||||
spyOn(FocusedContentStore, 'focusedId').andCallFake -> 'focus'
|
||||
|
||||
it "should turn the focused item into the first selected item", ->
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@dataView.selection.add).toHaveBeenCalledWith(@itemFocus)
|
||||
|
||||
it "should clear the focus", ->
|
||||
@handler.onMetaClick(@item)
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: null})
|
||||
|
||||
it "should expand selection", ->
|
||||
@handler.onShiftClick(@item)
|
||||
expect(@dataView.selection.expandTo).toHaveBeenCalledWith(@item)
|
||||
|
||||
it "should call _checkSelectionAndFocusConsistency", ->
|
||||
spyOn(@handler, '_checkSelectionAndFocusConsistency')
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()
|
||||
|
||||
describe "onEnter", ->
|
||||
|
||||
describe "onSelect (x key on keyboard)", ->
|
||||
it "should call _checkSelectionAndFocusConsistency", ->
|
||||
spyOn(@handler, '_checkSelectionAndFocusConsistency')
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()
|
||||
|
||||
describe "onShift", ->
|
||||
it "should call _checkSelectionAndFocusConsistency", ->
|
||||
spyOn(@handler, '_checkSelectionAndFocusConsistency')
|
||||
@handler.onMetaClick(@item)
|
||||
expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()
|
||||
|
||||
describe "when the select option is passed", ->
|
||||
it "should turn the existing focused item into a selected item", ->
|
||||
spyOn(FocusedContentStore, 'focused').andCallFake => @itemFocus
|
||||
spyOn(FocusedContentStore, 'focusedId').andCallFake -> 'focus'
|
||||
@handler.onShift(1, {select: true})
|
||||
expect(@dataView.selection.add).toHaveBeenCalledWith(@itemFocus)
|
||||
|
||||
it "should walk the selection to the shift target", ->
|
||||
spyOn(FocusedContentStore, 'focused').andCallFake => @itemFocus
|
||||
spyOn(FocusedContentStore, 'focusedId').andCallFake -> 'focus'
|
||||
@handler.onShift(1, {select: true})
|
||||
expect(@dataView.selection.walk).toHaveBeenCalledWith({current: @itemFocus, next: @itemAfterFocus})
|
||||
|
||||
describe "when one or more items is selected", ->
|
||||
it "should move the keyboard cursor", ->
|
||||
@selection = [@itemFocus, @itemAfterFocus, @itemKeyboardFocus]
|
||||
spyOn(FocusedContentStore, 'keyboardCursor').andCallFake => @itemKeyboardFocus
|
||||
spyOn(FocusedContentStore, 'keyboardCursorId').andCallFake -> 'keyboard-focus'
|
||||
@handler.onShift(1, {})
|
||||
expect(Actions.setCursorPosition).toHaveBeenCalledWith({collection: @collection, item: @itemAfterKeyboardFocus})
|
||||
|
||||
describe "when no items are selected", ->
|
||||
it "should move the focus", ->
|
||||
spyOn(FocusedContentStore, 'focused').andCallFake => @itemFocus
|
||||
spyOn(FocusedContentStore, 'focusedId').andCallFake -> 'focus'
|
||||
@handler.onShift(1, {})
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: @itemAfterFocus})
|
||||
|
||||
|
||||
describe "_checkSelectionAndFocusConsistency", ->
|
||||
describe "when only one item is selected", ->
|
||||
beforeEach ->
|
||||
spyOn(FocusedContentStore, 'focused').andCallFake -> null
|
||||
spyOn(FocusedContentStore, 'focusedId').andCallFake -> null
|
||||
@selection = [@item]
|
||||
|
||||
it "should clear the selection and make the item focused", ->
|
||||
@handler._checkSelectionAndFocusConsistency()
|
||||
expect(@dataView.selection.clear).toHaveBeenCalled()
|
||||
expect(Actions.setFocus).toHaveBeenCalledWith({collection: @collection, item: @item})
|
|
@ -7,7 +7,7 @@ Actions = require '../../src/flux/actions'
|
|||
testThread = new Thread(id: '123')
|
||||
|
||||
describe "FocusedContentStore", ->
|
||||
describe "onFocusInCollection", ->
|
||||
describe "onSetFocus", ->
|
||||
it "should not trigger if the thread is already focused", ->
|
||||
FocusedContentStore._onFocus({collection: 'thread', item: testThread})
|
||||
spyOn(FocusedContentStore, 'trigger')
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
{ComponentRegistry} = require 'nylas-exports'
|
||||
|
||||
ThreadListItemMixin = require './thread-list-item-mixin'
|
||||
|
||||
DefaultParticipants = React.createClass
|
||||
render: ->
|
||||
<div className="participants">
|
||||
{_.pluck(@props.participants, "email").join ", "}
|
||||
</div>
|
||||
|
||||
module.exports =
|
||||
ThreadListNarrowItem = React.createClass
|
||||
mixins: [ComponentRegistry.Mixin, ThreadListItemMixin]
|
||||
displayName: 'ThreadListNarrowItem'
|
||||
components: ["Participants"]
|
||||
|
||||
render: ->
|
||||
Participants = @state.Participants ? DefaultParticipants
|
||||
<div className={@_containerClasses()} onClick={@_onClick} id={@props.thread.id}>
|
||||
<div className="thread-title">
|
||||
<span className="btn-icon star-button pull-right"
|
||||
onClick={@_toggleStar}
|
||||
><i className={"fa " + (@_isStarred() and 'fa-star' or 'fa-star-o')}/></span>
|
||||
<div className="message-time">
|
||||
{@threadTime()}
|
||||
</div>
|
||||
<Participants participants={@props.thread.participants} clickable={false}/>
|
||||
</div>
|
||||
<div className="preview-body">
|
||||
<span className="subject">{@_subject()}</span>
|
||||
<span className="snippet">{@_snippet()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_containerClasses: ->
|
||||
classNames
|
||||
'unread': @props.unread
|
||||
'selected': @props.selected
|
||||
'thread-list-item': true
|
||||
'thread-list-narrow-item': true
|
|
@ -46,7 +46,7 @@ class ListTabularItem extends React.Component
|
|||
<div key={column.name}
|
||||
displayName={column.name}
|
||||
style={_.pick(column, ['flex', 'width'])}
|
||||
className="list-column">
|
||||
className="list-column list-column-#{column.name}">
|
||||
{column.resolver(@props.item, @)}
|
||||
</div>
|
||||
|
||||
|
@ -66,11 +66,15 @@ class ListTabular extends React.Component
|
|||
columns: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
|
||||
dataView: React.PropTypes.object
|
||||
itemPropsProvider: React.PropTypes.func
|
||||
itemHeight: React.PropTypes.number
|
||||
onSelect: React.PropTypes.func
|
||||
onClick: React.PropTypes.func
|
||||
onDoubleClick: React.PropTypes.func
|
||||
|
||||
constructor: (@props) ->
|
||||
if not @props.itemHeight
|
||||
throw new Error("ListTabular: You must provide an itemHeight - raising to avoid divide by zero errors.")
|
||||
|
||||
@state =
|
||||
renderedRangeStart: -1
|
||||
renderedRangeEnd: -1
|
||||
|
@ -103,7 +107,7 @@ class ListTabular extends React.Component
|
|||
|
||||
# If we've shifted enough pixels from our previous scrollTop to require
|
||||
# new rows to be rendered, update our state!
|
||||
if Math.abs(@state.scrollTop - @refs.container.scrollTop) >= @_rowHeight() * RangeChunkSize
|
||||
if Math.abs(@state.scrollTop - @refs.container.scrollTop) >= @props.itemHeight * RangeChunkSize
|
||||
@updateRangeState()
|
||||
|
||||
onDoneReceivingScrollEvents: =>
|
||||
|
@ -114,11 +118,9 @@ class ListTabular extends React.Component
|
|||
updateRangeState: =>
|
||||
scrollTop = @refs.container.scrollTop
|
||||
|
||||
rowHeight = @_rowHeight()
|
||||
|
||||
# Determine the exact range of rows we want onscreen
|
||||
rangeStart = Math.floor(scrollTop / rowHeight)
|
||||
rangeEnd = rangeStart + window.innerHeight / rowHeight
|
||||
rangeStart = Math.floor(scrollTop / @props.itemHeight)
|
||||
rangeEnd = rangeStart + window.innerHeight / @props.itemHeight
|
||||
|
||||
# 1. Clip this range to the number of available items
|
||||
#
|
||||
|
@ -154,7 +156,7 @@ class ListTabular extends React.Component
|
|||
|
||||
render: =>
|
||||
innerStyles =
|
||||
height: @props.dataView.count() * @_rowHeight()
|
||||
height: @props.dataView.count() * @props.itemHeight
|
||||
pointerEvents: if @state.scrollInProgress then 'none' else 'auto'
|
||||
|
||||
<ScrollRegion ref="container" onScroll={@updateScrollState} tabIndex="-1" className="list-container list-tabular" scrollTooltipComponent={@props.scrollTooltipComponent} >
|
||||
|
@ -164,9 +166,6 @@ class ListTabular extends React.Component
|
|||
</div>
|
||||
</ScrollRegion>
|
||||
|
||||
_rowHeight: =>
|
||||
39
|
||||
|
||||
_headers: =>
|
||||
return [] unless @props.displayHeaders
|
||||
|
||||
|
@ -182,7 +181,6 @@ class ListTabular extends React.Component
|
|||
</div>
|
||||
|
||||
_rows: =>
|
||||
rowHeight = @_rowHeight()
|
||||
rows = []
|
||||
|
||||
for idx in [@state.renderedRangeStart..@state.renderedRangeEnd-1]
|
||||
|
@ -196,7 +194,7 @@ class ListTabular extends React.Component
|
|||
rows.push <ListTabularItem key={item.id ? idx}
|
||||
item={item}
|
||||
itemProps={itemProps}
|
||||
metrics={top: idx * rowHeight, height: rowHeight}
|
||||
metrics={top: idx * @props.itemHeight, height: @props.itemHeight}
|
||||
columns={@props.columns}
|
||||
onSelect={@props.onSelect}
|
||||
onClick={@props.onClick}
|
||||
|
|
54
src/components/multiselect-list-interaction-handler.coffee
Normal file
54
src/components/multiselect-list-interaction-handler.coffee
Normal file
|
@ -0,0 +1,54 @@
|
|||
_ = require 'underscore'
|
||||
{Actions,
|
||||
WorkspaceStore,
|
||||
FocusedContentStore} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
class MultiselectListInteractionHandler
|
||||
constructor: (@dataView, @collection) ->
|
||||
|
||||
shouldShowFocus: ->
|
||||
false
|
||||
|
||||
shouldShowKeyboardCursor: ->
|
||||
true
|
||||
|
||||
onClick: (item) ->
|
||||
Actions.setFocus({collection: @collection, item: item})
|
||||
|
||||
onMetaClick: (item) ->
|
||||
@dataView.selection.toggle(item)
|
||||
Actions.setCursorPosition({collection: @collection, item: item})
|
||||
|
||||
onShiftClick: (item) ->
|
||||
@dataView.selection.expandTo(item)
|
||||
Actions.setCursorPosition({collection: @collection, item: item})
|
||||
|
||||
onEnter: ->
|
||||
keyboardCursorId = FocusedContentStore.keyboardCursorId(@collection)
|
||||
if keyboardCursorId
|
||||
item = @dataView.getById(keyboardCursorId)
|
||||
Actions.setFocus({collection: @collection, item: item})
|
||||
|
||||
onSelect: ->
|
||||
{id} = @_keyboardContext()
|
||||
return unless id
|
||||
@dataView.selection.toggle(@dataView.getById(id))
|
||||
|
||||
onShift: (delta, options = {}) ->
|
||||
{id, action} = @_keyboardContext()
|
||||
|
||||
current = @dataView.getById(id)
|
||||
index = @dataView.indexOfId(id)
|
||||
index = Math.max(0, Math.min(index + delta, @dataView.count() - 1))
|
||||
next = @dataView.get(index)
|
||||
|
||||
action({collection: @collection, item: next})
|
||||
if options.select
|
||||
@dataView.selection.walk({current, next})
|
||||
|
||||
_keyboardContext: ->
|
||||
if WorkspaceStore.topSheet().root
|
||||
{id: FocusedContentStore.keyboardCursorId(@collection), action: Actions.setCursorPosition}
|
||||
else
|
||||
{id: FocusedContentStore.focusedId(@collection), action: Actions.setFocus}
|
|
@ -11,6 +11,9 @@ Spinner = require './spinner'
|
|||
NamespaceStore} = require 'nylas-exports'
|
||||
EventEmitter = require('events').EventEmitter
|
||||
|
||||
MultiselectListInteractionHandler = require './multiselect-list-interaction-handler'
|
||||
MultiselectSplitInteractionHandler = require './multiselect-split-interaction-handler'
|
||||
|
||||
###
|
||||
Public: MultiselectList wraps {ListTabular} and makes it easy to present a
|
||||
{ModelView} with selection support. It adds a checkbox column to the columns
|
||||
|
@ -32,6 +35,7 @@ class MultiselectList extends React.Component
|
|||
columns: React.PropTypes.array.isRequired
|
||||
dataStore: React.PropTypes.object.isRequired
|
||||
itemPropsProvider: React.PropTypes.func.isRequired
|
||||
itemHeight: React.PropTypes.number.isRequired
|
||||
scrollTooltipComponent: React.PropTypes.func
|
||||
|
||||
constructor: (@props) ->
|
||||
|
@ -77,21 +81,9 @@ class MultiselectList extends React.Component
|
|||
context = {focusedId: @state.focusedId}
|
||||
props.commands[key](context)
|
||||
|
||||
unless props.columns[0].name is 'Check'
|
||||
checkmarkColumn = new ListTabular.Column
|
||||
name: "Check"
|
||||
resolver: (thread) =>
|
||||
toggle = (event) =>
|
||||
if event.shiftKey
|
||||
props.dataStore.view().selection.expandTo(thread)
|
||||
else
|
||||
props.dataStore.view().selection.toggle(thread)
|
||||
event.stopPropagation()
|
||||
<div className="checkmark" onClick={toggle}><div className="inner"></div></div>
|
||||
props.columns.splice(0, 0, checkmarkColumn)
|
||||
|
||||
@unsubscribers = []
|
||||
@unsubscribers.push props.dataStore.listen @_onChange
|
||||
@unsubscribers.push WorkspaceStore.listen @_onChange
|
||||
@unsubscribers.push FocusedContentStore.listen @_onChange
|
||||
@command_unsubscriber = atom.commands.add('body', commands)
|
||||
|
||||
|
@ -111,18 +103,19 @@ class MultiselectList extends React.Component
|
|||
props.className ?= ''
|
||||
props.className += " " + classNames
|
||||
'selected': item.id in @state.selectedIds
|
||||
'focused': @state.showFocus and item.id is @state.focusedId
|
||||
'keyboard-cursor': @state.showKeyboardCursor and item.id is @state.keyboardCursorId
|
||||
'focused': @state.handler.shouldShowFocus() and item.id is @state.focusedId
|
||||
'keyboard-cursor': @state.handler.shouldShowKeyboardCursor() and item.id is @state.keyboardCursorId
|
||||
props
|
||||
|
||||
if @state.dataView
|
||||
<div className={className}>
|
||||
<ListTabular
|
||||
ref="list"
|
||||
columns={@props.columns}
|
||||
columns={@state.columns}
|
||||
scrollTooltipComponent={@props.scrollTooltipComponent}
|
||||
dataView={@state.dataView}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={@props.itemHeight}
|
||||
onSelect={@_onClickItem}
|
||||
onDoubleClick={@props.onDoubleClick} />
|
||||
<Spinner visible={!@state.ready} />
|
||||
|
@ -135,58 +128,24 @@ class MultiselectList extends React.Component
|
|||
|
||||
_onClickItem: (item, event) =>
|
||||
if event.metaKey
|
||||
@state.dataView.selection.toggle(item)
|
||||
if @state.showKeyboardCursor
|
||||
Actions.focusKeyboardInCollection({collection: @props.collection, item: item})
|
||||
@state.handler.onMetaClick(item)
|
||||
else if event.shiftKey
|
||||
@state.dataView.selection.expandTo(item)
|
||||
if @state.showKeyboardCursor
|
||||
Actions.focusKeyboardInCollection({collection: @props.collection, item: item})
|
||||
@state.handler.onShiftClick(item)
|
||||
else
|
||||
Actions.focusInCollection({collection: @props.collection, item: item})
|
||||
@state.handler.onClick(item)
|
||||
|
||||
_onEnter: =>
|
||||
return unless @state.showKeyboardCursor
|
||||
item = @state.dataView.getById(@state.keyboardCursorId)
|
||||
if item
|
||||
Actions.focusInCollection({collection: @props.collection, item: item})
|
||||
@state.handler.onEnter()
|
||||
|
||||
_onSelect: =>
|
||||
if @state.showKeyboardCursor and @_visible()
|
||||
id = @state.keyboardCursorId
|
||||
else
|
||||
id = @state.focusedId
|
||||
|
||||
return unless id
|
||||
@state.dataView.selection.toggle(@state.dataView.getById(id))
|
||||
@state.handler.onSelect()
|
||||
|
||||
_onDeselect: =>
|
||||
return unless @_visible()
|
||||
@state.dataView.selection.clear()
|
||||
|
||||
_onShift: (delta, options = {}) =>
|
||||
if @state.showKeyboardCursor and @_visible()
|
||||
id = @state.keyboardCursorId
|
||||
action = Actions.focusKeyboardInCollection
|
||||
else
|
||||
id = @state.focusedId
|
||||
action = Actions.focusInCollection
|
||||
|
||||
current = @state.dataView.getById(id)
|
||||
index = @state.dataView.indexOfId(id)
|
||||
index = Math.max(0, Math.min(index + delta, @state.dataView.count() - 1))
|
||||
next = @state.dataView.get(index)
|
||||
|
||||
action({collection: @props.collection, item: next})
|
||||
|
||||
if options.select
|
||||
@state.dataView.selection.walk({current, next})
|
||||
|
||||
_visible: =>
|
||||
if WorkspaceStore.layoutMode() is "list"
|
||||
WorkspaceStore.topSheet().root
|
||||
else
|
||||
true
|
||||
@state.handler.onShift(delta, options)
|
||||
|
||||
# This onChange handler can be called many times back to back and setState
|
||||
# sometimes triggers an immediate render. Ensure that we never render back-to-back,
|
||||
|
@ -198,19 +157,44 @@ class MultiselectList extends React.Component
|
|||
, 1
|
||||
@_onChangeDebounced()
|
||||
|
||||
_visible: =>
|
||||
if WorkspaceStore.layoutMode() is "list"
|
||||
WorkspaceStore.topSheet().root
|
||||
else
|
||||
true
|
||||
|
||||
_getCheckmarkColumn: =>
|
||||
new ListTabular.Column
|
||||
name: 'Check'
|
||||
resolver: (item) =>
|
||||
toggle = (event) =>
|
||||
if event.shiftKey
|
||||
@state.handler.onShiftClick(item)
|
||||
else
|
||||
@state.handler.onMetaClick(item)
|
||||
event.stopPropagation()
|
||||
<div className="checkmark" onClick={toggle}><div className="inner"></div></div>
|
||||
|
||||
_getStateFromStores: (props) =>
|
||||
props ?= @props
|
||||
|
||||
view = props.dataStore?.view()
|
||||
return {} unless view
|
||||
|
||||
columns = [].concat(props.columns)
|
||||
|
||||
if WorkspaceStore.layoutMode() is 'list'
|
||||
handler = new MultiselectListInteractionHandler(view, props.collection)
|
||||
columns.splice(0, 0, @_getCheckmarkColumn())
|
||||
else
|
||||
handler = new MultiselectSplitInteractionHandler(view, props.collection)
|
||||
|
||||
dataView: view
|
||||
columns: columns
|
||||
handler: handler
|
||||
ready: view.loaded()
|
||||
selectedIds: view.selection.ids()
|
||||
focusedId: FocusedContentStore.focusedId(props.collection)
|
||||
keyboardCursorId: FocusedContentStore.keyboardCursorId(props.collection)
|
||||
showFocus: !FocusedContentStore.keyboardCursorEnabled()
|
||||
showKeyboardCursor: FocusedContentStore.keyboardCursorEnabled()
|
||||
|
||||
|
||||
module.exports = MultiselectList
|
||||
|
|
75
src/components/multiselect-split-interaction-handler.coffee
Normal file
75
src/components/multiselect-split-interaction-handler.coffee
Normal file
|
@ -0,0 +1,75 @@
|
|||
_ = require 'underscore'
|
||||
{Actions,
|
||||
WorkspaceStore,
|
||||
FocusedContentStore} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
class MultiselectSplitInteractionHandler
|
||||
constructor: (@dataView, @collection) ->
|
||||
|
||||
shouldShowFocus: ->
|
||||
true
|
||||
|
||||
shouldShowKeyboardCursor: ->
|
||||
@dataView.selection.count() > 1
|
||||
|
||||
onClick: (item) ->
|
||||
Actions.setFocus({collection: @collection, item: item})
|
||||
@dataView.selection.clear()
|
||||
@_checkSelectionAndFocusConsistency()
|
||||
|
||||
onMetaClick: (item) ->
|
||||
@_turnFocusIntoSelection()
|
||||
@dataView.selection.toggle(item)
|
||||
@_checkSelectionAndFocusConsistency()
|
||||
|
||||
onShiftClick: (item) ->
|
||||
@_turnFocusIntoSelection()
|
||||
@dataView.selection.expandTo(item)
|
||||
@_checkSelectionAndFocusConsistency()
|
||||
|
||||
onEnter: ->
|
||||
|
||||
onSelect: ->
|
||||
@_checkSelectionAndFocusConsistency()
|
||||
|
||||
onShift: (delta, options) ->
|
||||
if options.select
|
||||
@_turnFocusIntoSelection()
|
||||
|
||||
if @dataView.selection.count() > 0
|
||||
selection = @dataView.selection
|
||||
keyboardId = FocusedContentStore.keyboardCursorId(@collection)
|
||||
id = keyboardId ? @dataView.selection.top().id
|
||||
action = Actions.setCursorPosition
|
||||
else
|
||||
id = FocusedContentStore.focusedId(@collection)
|
||||
action = Actions.setFocus
|
||||
|
||||
current = @dataView.getById(id)
|
||||
index = @dataView.indexOfId(id)
|
||||
index = Math.max(0, Math.min(index + delta, @dataView.count() - 1))
|
||||
next = @dataView.get(index)
|
||||
|
||||
action({collection: @collection, item: next})
|
||||
if options.select
|
||||
@dataView.selection.walk({current, next})
|
||||
|
||||
@_checkSelectionAndFocusConsistency()
|
||||
|
||||
_turnFocusIntoSelection: ->
|
||||
focused = FocusedContentStore.focused(@collection)
|
||||
Actions.setFocus({collection: @collection, item: null})
|
||||
@dataView.selection.add(focused)
|
||||
|
||||
_checkSelectionAndFocusConsistency: ->
|
||||
focused = FocusedContentStore.focused(@collection)
|
||||
selection = @dataView.selection
|
||||
|
||||
if focused and selection.count() > 0
|
||||
@dataView.selection.add(focused)
|
||||
Actions.setFocus({collection: @collection, item: null})
|
||||
|
||||
if selection.count() is 1 and !focused
|
||||
Actions.setFocus({collection: @collection, item: selection.items()[0]})
|
||||
@dataView.selection.clear()
|
|
@ -185,7 +185,7 @@ class Actions
|
|||
Actions.selectLayoutMode(collection: 'thread', item: <Thread>)
|
||||
```
|
||||
###
|
||||
@focusKeyboardInCollection: ActionScopeWindow
|
||||
@setCursorPosition: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Focus on an item in a collection. This action changes the selection
|
||||
|
@ -194,10 +194,10 @@ class Actions
|
|||
*Scope: Window*
|
||||
|
||||
```
|
||||
Actions.focusInCollection(collection: 'thread', item: <Thread>)
|
||||
Actions.setFocus(collection: 'thread', item: <Thread>)
|
||||
```
|
||||
###
|
||||
@focusInCollection: ActionScopeWindow
|
||||
@setFocus: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Focus the interface on a specific {Tag}.
|
||||
|
@ -226,7 +226,7 @@ class Actions
|
|||
###
|
||||
Public: Create a new reply to the provided threadId and messageId. Note that
|
||||
this action does not focus on the thread, so you may not be able to see the new draft
|
||||
unless you also call {::focusInCollection}.
|
||||
unless you also call {::setFocus}.
|
||||
|
||||
*Scope: Window*
|
||||
|
||||
|
@ -435,7 +435,7 @@ class Actions
|
|||
###
|
||||
Public: Push a sheet of a specific type onto the Sheet stack maintained by the
|
||||
{WorkspaceStore}. Note that sheets have no state. To show a *specific* thread,
|
||||
you should push a Thread sheet and call `focusInCollection` to select the thread.
|
||||
you should push a Thread sheet and call `setFocus` to select the thread.
|
||||
|
||||
*Scope: Window*
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ selection public so that you can observe focus changes and trigger your own chan
|
|||
to focus.
|
||||
|
||||
Since {FocusedContentStore} is a Flux-compatible Store, you do not call setters
|
||||
on it directly. Instead, use {Actions::focusInCollection} or
|
||||
{Actions::focusKeyboardInCollection} to set focus. The FocusedContentStore observes
|
||||
on it directly. Instead, use {Actions::setFocus} or
|
||||
{Actions::setCursorPosition} to set focus. The FocusedContentStore observes
|
||||
these models, changes it's state, and broadcasts to it's observers.
|
||||
|
||||
Note: The {FocusedContentStore} triggers when a focused model is changed, even if
|
||||
|
@ -62,8 +62,8 @@ class FocusedContentStore
|
|||
@listenTo NamespaceStore, @_onClear
|
||||
@listenTo WorkspaceStore, @_onWorkspaceChange
|
||||
@listenTo DatabaseStore, @_onDataChange
|
||||
@listenTo Actions.focusInCollection, @_onFocus
|
||||
@listenTo Actions.focusKeyboardInCollection, @_onFocusKeyboard
|
||||
@listenTo Actions.setFocus, @_onFocus
|
||||
@listenTo Actions.setCursorPosition, @_onFocusKeyboard
|
||||
|
||||
_resetInstanceVars: =>
|
||||
@_focused = {}
|
||||
|
|
|
@ -17,6 +17,9 @@ class ModelViewSelection
|
|||
items: ->
|
||||
@_items
|
||||
|
||||
top: ->
|
||||
@_items[@_items.length - 1]
|
||||
|
||||
clear: ->
|
||||
@set([])
|
||||
|
||||
|
@ -49,6 +52,16 @@ class ModelViewSelection
|
|||
@_items.push(item)
|
||||
@trigger(@)
|
||||
|
||||
add: (item) ->
|
||||
return unless item
|
||||
throw new Error("add must be called with a Model") unless item instanceof Model
|
||||
|
||||
updated = _.reject @_items, (t) -> t.id is item.id
|
||||
updated.push(item)
|
||||
if updated.length isnt @_items.length
|
||||
@_items = updated
|
||||
@trigger(@)
|
||||
|
||||
remove: (item) ->
|
||||
return unless item
|
||||
throw new Error("remove must be called with a Model") unless item instanceof Model
|
||||
|
|
|
@ -28,7 +28,7 @@ class WorkspaceStore
|
|||
|
||||
@listenTo Actions.selectRootSheet, @_onSelectRootSheet
|
||||
@listenTo Actions.selectLayoutMode, @_onSelectLayoutMode
|
||||
@listenTo Actions.focusInCollection, @_onFocusInCollection
|
||||
@listenTo Actions.setFocus, @_onSetFocus
|
||||
|
||||
@listenTo Actions.popSheet, @popSheet
|
||||
@listenTo Actions.searchQueryCommitted, @popToRootSheet
|
||||
|
@ -74,7 +74,7 @@ class WorkspaceStore
|
|||
@_preferredLayoutMode = mode
|
||||
@trigger(@)
|
||||
|
||||
_onFocusInCollection: ({collection, item}) =>
|
||||
_onSetFocus: ({collection, item}) =>
|
||||
if collection is 'thread'
|
||||
if @layoutMode() is 'list'
|
||||
if item and @topSheet() isnt Sheet.Thread
|
||||
|
@ -182,7 +182,7 @@ class WorkspaceStore
|
|||
@trigger()
|
||||
|
||||
if Sheet.Thread and sheet is Sheet.Thread
|
||||
Actions.focusInCollection(collection: 'thread', item: null)
|
||||
Actions.setFocus(collection: 'thread', item: null)
|
||||
|
||||
# Return to the root sheet. This method triggers, allowing observers
|
||||
# to update.
|
||||
|
|
|
@ -36,7 +36,7 @@ class SheetContainer extends React.Component
|
|||
|
||||
<Flexbox direction="column">
|
||||
{@_toolbarContainerElement()}
|
||||
|
||||
|
||||
<div name="Header" style={order:1, zIndex: 2}>
|
||||
<InjectedComponentSet matching={locations: [topSheet.Header, WorkspaceStore.Sheet.Global.Header]}
|
||||
direction="column"
|
||||
|
@ -89,6 +89,7 @@ class SheetContainer extends React.Component
|
|||
|
||||
_onColumnSizeChanged: (sheet) =>
|
||||
@refs["toolbar-#{sheet.props.depth}"]?.recomputeLayout()
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
|
||||
_onStoreChange: =>
|
||||
_.defer => @setState(@_getStateFromStores())
|
||||
|
@ -97,4 +98,4 @@ class SheetContainer extends React.Component
|
|||
stack: WorkspaceStore.sheetStack()
|
||||
|
||||
|
||||
module.exports = SheetContainer
|
||||
module.exports = SheetContainer
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
}
|
||||
|
||||
&.keyboard-cursor {
|
||||
.checkmark {
|
||||
.list-column:first-child {
|
||||
border-left:4px solid @list-focused-bg;
|
||||
padding-left:8px;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue