mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-25 17:42:16 +08:00
feat(drag-threads): Move threads to folders/labels from thread list
Summary: Drag threads to the folders / labels in the sidebar WIP Drag and drop is styled! Test Plan: Tests WIP Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1785
This commit is contained in:
parent
4be05ff754
commit
a559e3f89f
14 changed files with 236 additions and 80 deletions
|
@ -199,8 +199,6 @@ module.exports = (grunt) ->
|
|||
sidebarPath = path.join(docsOutputDir, '_sidebar.json')
|
||||
grunt.file.write(sidebarPath, sidebarJson)
|
||||
|
||||
|
||||
|
||||
# Prepare to render by loading handlebars partials
|
||||
|
||||
templatesPath = path.resolve(__dirname, '..', '..', 'docs-templates')
|
||||
|
@ -214,40 +212,6 @@ module.exports = (grunt) ->
|
|||
for classname, val of apiJSON.classes
|
||||
knownClassnames[classname.toLowerCase()] = val
|
||||
|
||||
expandTypeReferences = (val) ->
|
||||
refRegex = /{([\w.]*)}/g
|
||||
while (match = refRegex.exec(val)) isnt null
|
||||
term = match[1].toLowerCase()
|
||||
label = match[1]
|
||||
url = false
|
||||
if term in standardClasses
|
||||
url = standardClassURLRoot+term
|
||||
else if thirdPartyClasses[term]
|
||||
url = thirdPartyClasses[term]
|
||||
else if knownClassnames[term]
|
||||
url = relativePathForClass(term)
|
||||
else
|
||||
console.warn("Cannot find class named #{term}")
|
||||
|
||||
if url
|
||||
val = val.replace(match[0], "<a href='#{url}'>#{label}</a>")
|
||||
val
|
||||
|
||||
expandFuncReferences = (val) ->
|
||||
refRegex = /{([\w]*)?::([\w]*)}/g
|
||||
while (match = refRegex.exec(val)) isnt null
|
||||
[text, a, b] = match
|
||||
url = false
|
||||
if a and b
|
||||
url = "#{relativePathForClass(a)}##{b}"
|
||||
label = "#{a}::#{b}"
|
||||
else
|
||||
url = "##{b}"
|
||||
label = "#{b}"
|
||||
if url
|
||||
val = val.replace(text, "<a href='#{url}'>#{label}</a>")
|
||||
val
|
||||
|
||||
# Render Class Pages
|
||||
|
||||
classTemplatePath = path.join(templatesPath, 'class.html')
|
||||
|
@ -256,8 +220,9 @@ module.exports = (grunt) ->
|
|||
for {name, documentation, section} in classes
|
||||
# Recursively process `description` and `type` fields to process markdown,
|
||||
# expand references to types, functions and other files.
|
||||
processFields(documentation, ['description'], [marked.noMeta, expandTypeReferences, expandFuncReferences])
|
||||
processFields(documentation, ['type'], [expandTypeReferences])
|
||||
processFields(documentation, ['description'], [marked.noMeta])
|
||||
processFields(documentation, ['type'], [])
|
||||
|
||||
result = classTemplate({name, documentation, section})
|
||||
console.log(outputPathFor(relativePathForClass(name)))
|
||||
grunt.file.write(outputPathFor(relativePathForClass(name)), result)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
module.exports =
|
||||
# Models
|
||||
Menu: require '../src/components/menu'
|
||||
DropZone: require '../src/components/drop-zone'
|
||||
Spinner: require '../src/components/spinner'
|
||||
Popover: require '../src/components/popover'
|
||||
Flexbox: require '../src/components/flexbox'
|
||||
|
|
|
@ -45,6 +45,7 @@ Exports =
|
|||
# Utils
|
||||
Utils: Utils
|
||||
DOMUtils: require '../src/dom-utils'
|
||||
CanvasUtils: require '../src/canvas-utils'
|
||||
RegExpUtils: require '../src/regexp-utils'
|
||||
MessageUtils: require '../src/flux/models/message-utils'
|
||||
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{Actions, Utils, UnreadCountStore, WorkspaceStore} = require 'nylas-exports'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{Actions,
|
||||
Utils,
|
||||
UnreadCountStore,
|
||||
WorkspaceStore,
|
||||
NamespaceStore,
|
||||
FocusedCategoryStore,
|
||||
ChangeLabelsTask,
|
||||
ChangeFolderTask,
|
||||
CategoryStore} = require 'nylas-exports'
|
||||
{RetinaImg, DropZone} = require 'nylas-component-kit'
|
||||
|
||||
class AccountSidebarCategoryItem extends React.Component
|
||||
@displayName: 'AccountSidebarCategoryItem'
|
||||
|
@ -19,9 +27,8 @@ class AccountSidebarCategoryItem extends React.Component
|
|||
_onUnreadCountChange: =>
|
||||
@setState unreadCount: UnreadCountStore.count()
|
||||
|
||||
shouldComponentUpdate: (nextProps) =>
|
||||
@props?.item.name isnt nextProps.item.name or
|
||||
@props?.select isnt nextProps.select
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
!Utils.isEqualReact(@props, nextProps) or !Utils.isEqualReact(@state, nextState)
|
||||
|
||||
render: =>
|
||||
unread = []
|
||||
|
@ -31,12 +38,47 @@ class AccountSidebarCategoryItem extends React.Component
|
|||
containerClass = classNames
|
||||
'item': true
|
||||
'selected': @props.select
|
||||
'dropping': @state.isDropping
|
||||
|
||||
<div className={containerClass} onClick={@_onClick} id={@props.item.id}>
|
||||
<DropZone className={containerClass}
|
||||
onClick={@_onClick}
|
||||
id={@props.item.id}
|
||||
shouldAcceptDrop={@_shouldAcceptDrop}
|
||||
onDragStateChange={ ({isDropping}) => @setState({isDropping}) }
|
||||
onDrop={@_onDrop}>
|
||||
{unread}
|
||||
<RetinaImg name={"#{@props.item.name}.png"} fallback={'folder.png'} mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<span className="name"> {@props.item.displayName}</span>
|
||||
</div>
|
||||
</DropZone>
|
||||
|
||||
_shouldAcceptDrop: (e) =>
|
||||
return false if @props.item.name in CategoryStore.LockedCategoryNames
|
||||
return false if @props.item.name is FocusedCategoryStore.categoryName()
|
||||
'nylas-thread-ids' in e.dataTransfer.types
|
||||
|
||||
_onDrop: (e) =>
|
||||
jsonString = e.dataTransfer.getData('nylas-thread-ids')
|
||||
try
|
||||
ids = JSON.parse(jsonString)
|
||||
catch err
|
||||
console.error("AccountSidebarCategoryItem onDrop: JSON parse #{err}")
|
||||
return unless ids
|
||||
|
||||
if NamespaceStore.current().usesLabels()
|
||||
currentLabel = FocusedCategoryStore.category()
|
||||
if currentLabel and not (currentLabel in CategoryStore.LockedCategoryNames)
|
||||
labelsToRemove = [currentLabel]
|
||||
|
||||
task = new ChangeLabelsTask
|
||||
threadIds: ids,
|
||||
labelsToAdd: [@props.item],
|
||||
labelsToRemove: labelsToRemove
|
||||
else
|
||||
task = new ChangeFolderTask
|
||||
folderOrId: @props.item,
|
||||
threadIds: ids
|
||||
|
||||
Actions.queueTask(task)
|
||||
|
||||
_onClick: (event) =>
|
||||
event.preventDefault()
|
||||
|
|
|
@ -49,6 +49,11 @@
|
|||
color: @source-list-active-color;
|
||||
img.content-mask { background-color: @source-list-active-color; }
|
||||
}
|
||||
&.dropping {
|
||||
background-color: lighten(@source-list-bg, 20%);
|
||||
color: @source-list-active-color;
|
||||
img.content-mask { background-color: @source-list-active-color; }
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
|
|
|
@ -14,6 +14,7 @@ _ = require 'underscore'
|
|||
InjectedComponentSet,
|
||||
InjectedComponent,
|
||||
ScrollRegion,
|
||||
DropZone,
|
||||
RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
FileUpload = require './file-upload'
|
||||
|
@ -159,7 +160,10 @@ class ComposerView extends React.Component
|
|||
"composer-outer-wrap #{@props.className ? ""}"
|
||||
|
||||
_renderComposer: =>
|
||||
<div className="composer-inner-wrap" onDragEnter={@_onDragEnter} onDragLeave={@_onDragLeave} onDrop={@_onDrop}>
|
||||
<DropZone className="composer-inner-wrap"
|
||||
shouldAcceptDrop={@_shouldAcceptDrop}
|
||||
onDragStateChange={ ({isDropping}) => @setState({isDropping}) }
|
||||
onDrop={@_onDrop}>
|
||||
<div className="composer-cover" style={display: if @state.isSending then 'block' else 'none'}></div>
|
||||
<div className="composer-drop-cover" style={display: if @state.isDropping then 'block' else 'none'}>
|
||||
<div className="centered">
|
||||
|
@ -207,7 +211,7 @@ class ComposerView extends React.Component
|
|||
<div className="composer-action-bar-wrap">
|
||||
{@_renderActionsRegion()}
|
||||
</div>
|
||||
</div>
|
||||
</DropZone>
|
||||
|
||||
_renderFields: =>
|
||||
# Note: We need to physically add and remove these elements, not just hide them.
|
||||
|
@ -470,7 +474,7 @@ class ComposerView extends React.Component
|
|||
else if @isForwardedMessage() then return true
|
||||
else return false
|
||||
|
||||
_shouldAcceptDrop: (event) ->
|
||||
_shouldAcceptDrop: (event) =>
|
||||
# Ensure that you can't pick up a file and drop it on the same draft
|
||||
existingFilePaths = @state.files.map (f) ->
|
||||
FileUploadStore.linkedUpload(f)?.filePath
|
||||
|
@ -484,7 +488,7 @@ class ComposerView extends React.Component
|
|||
|
||||
return hasNativeFile or hasNonNativeFilePath
|
||||
|
||||
_nonNativeFilePathForDrop: (event) ->
|
||||
_nonNativeFilePathForDrop: (event) =>
|
||||
if "text/nylas-file-url" in event.dataTransfer.types
|
||||
downloadURL = event.dataTransfer.getData("text/nylas-file-url")
|
||||
downloadFilePath = downloadURL.split('file://')[1]
|
||||
|
@ -500,30 +504,7 @@ class ComposerView extends React.Component
|
|||
|
||||
return null
|
||||
|
||||
# We maintain a "dragCounter" because dragEnter and dragLeave events *stack*
|
||||
# when the user moves the item in and out of DOM elements inside of our container.
|
||||
# It's really awful and everyone hates it.
|
||||
#
|
||||
# Alternative solution *maybe* is to set pointer-events:none; during drag.
|
||||
|
||||
_onDragEnter: (e) =>
|
||||
return unless @_shouldAcceptDrop(e)
|
||||
@_dragCounter ?= 0
|
||||
@_dragCounter += 1
|
||||
if @_dragCounter is 1
|
||||
@setState(isDropping: true)
|
||||
|
||||
_onDragLeave: (e) =>
|
||||
return unless @_shouldAcceptDrop(e)
|
||||
@_dragCounter -= 1
|
||||
if @_dragCounter is 0
|
||||
@setState(isDropping: false)
|
||||
|
||||
_onDrop: (e) =>
|
||||
return unless @_shouldAcceptDrop(e)
|
||||
@setState(isDropping: false)
|
||||
@_dragCounter = 0
|
||||
|
||||
# Accept drops of real files from other applications
|
||||
for file in e.dataTransfer.files
|
||||
Actions.attachFilePath({path: file.path, messageLocalId: @props.localId})
|
||||
|
@ -532,8 +513,6 @@ class ComposerView extends React.Component
|
|||
if (uri = @_nonNativeFilePathForDrop(e))
|
||||
Actions.attachFilePath({path: uri, messageLocalId: @props.localId})
|
||||
|
||||
return
|
||||
|
||||
_onFilePaste: (path) =>
|
||||
Actions.attachFilePath({path: path, messageLocalId: @props.localId})
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ classNames = require 'classnames'
|
|||
{timestamp, subject} = require './formatting-utils'
|
||||
{Actions,
|
||||
Utils,
|
||||
CanvasUtils,
|
||||
Thread,
|
||||
WorkspaceStore,
|
||||
NamespaceStore,
|
||||
|
@ -152,6 +153,7 @@ class ThreadList extends React.Component
|
|||
@itemPropsProvider = (item) ->
|
||||
className: classNames
|
||||
'unread': item.unread
|
||||
'data-thread-id': item.id
|
||||
|
||||
componentDidMount: =>
|
||||
window.addEventListener('resize', @_onResize, true)
|
||||
|
@ -171,6 +173,9 @@ class ThreadList extends React.Component
|
|||
className="thread-list"
|
||||
scrollTooltipComponent={ThreadListScrollTooltip}
|
||||
emptyComponent={EmptyState}
|
||||
onDragStart={@_onDragStart}
|
||||
onDragEnd={@_onDragEnd}
|
||||
draggable="true"
|
||||
collection="thread" />
|
||||
else if @state.style is 'narrow'
|
||||
<MultiselectList
|
||||
|
@ -186,6 +191,32 @@ class ThreadList extends React.Component
|
|||
else
|
||||
<div></div>
|
||||
|
||||
_threadIdAtPoint: (x, y) ->
|
||||
item = document.elementFromPoint(event.clientX, event.clientY).closest('.list-item')
|
||||
return null unless item
|
||||
return item.dataset.threadId
|
||||
|
||||
_onDragStart: (event) =>
|
||||
itemThreadId = @_threadIdAtPoint(event.clientX, event.clientY)
|
||||
unless itemThreadId
|
||||
event.preventDefault()
|
||||
return
|
||||
|
||||
if itemThreadId in ThreadListStore.view().selection.ids()
|
||||
dragThreadIds = ThreadListStore.view().selection.ids()
|
||||
else
|
||||
dragThreadIds = [itemThreadId]
|
||||
|
||||
event.dataTransfer.effectAllowed = "move"
|
||||
event.dataTransfer.dragEffect = "move"
|
||||
|
||||
canvas = CanvasUtils.canvasWithThreadDragImage(dragThreadIds.length)
|
||||
event.dataTransfer.setDragImage(canvas, 10, 10)
|
||||
event.dataTransfer.setData('nylas-thread-ids', JSON.stringify(dragThreadIds))
|
||||
return
|
||||
|
||||
_onDragEnd: (event) =>
|
||||
|
||||
_onResize: (event) =>
|
||||
current = @state.style
|
||||
desired = if React.findDOMNode(@).offsetWidth < 540 then 'narrow' else 'wide'
|
||||
|
|
73
src/canvas-utils.coffee
Normal file
73
src/canvas-utils.coffee
Normal file
|
@ -0,0 +1,73 @@
|
|||
ThreadDragImage = document.createElement("img")
|
||||
ThreadDragImage.src = """data:image/png;base64,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"""
|
||||
|
||||
DragCanvas = document.createElement("canvas")
|
||||
document.body.appendChild(DragCanvas)
|
||||
|
||||
CanvasUtils =
|
||||
roundRect: (ctx, x, y, width, height, radius = 5, fill, stroke = true) ->
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + radius, y)
|
||||
ctx.lineTo(x + width - radius, y)
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
|
||||
ctx.lineTo(x + width, y + height - radius)
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
|
||||
ctx.lineTo(x + radius, y + height)
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
|
||||
ctx.lineTo(x, y + radius)
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y)
|
||||
ctx.closePath()
|
||||
ctx.stroke() if stroke
|
||||
ctx.fill() if fill
|
||||
|
||||
canvasWithThreadDragImage: (count) ->
|
||||
canvas = DragCanvas
|
||||
|
||||
# Make sure the canvas has a 2x pixel density on retina displays
|
||||
scale = window.devicePixelRatio
|
||||
canvas.width = 58 * scale
|
||||
canvas.height = 55 * scale
|
||||
canvas.style.position = "absolute"
|
||||
canvas.style.width = "58px"
|
||||
canvas.style.height = "55px"
|
||||
|
||||
# necessary for setDragImage to work
|
||||
ctx = canvas.getContext('2d')
|
||||
|
||||
# mail background image
|
||||
if count > 1
|
||||
ctx.rotate(-20*Math.PI/180)
|
||||
ctx.drawImage(ThreadDragImage, -10*scale, 2*scale, 48*scale, 48*scale)
|
||||
ctx.rotate(20*Math.PI/180)
|
||||
ctx.drawImage(ThreadDragImage, 0, 0, 48*scale, 48*scale)
|
||||
|
||||
# count bubble
|
||||
dotGradient = ctx.createLinearGradient(0, 0, 0, 15 * scale)
|
||||
dotGradient.addColorStop(0, "rgb(116, 124, 143)")
|
||||
dotGradient.addColorStop(1, "rgb(67, 77, 104)")
|
||||
ctx.strokeStyle = "rgba(39, 48, 68, 0.6)"
|
||||
ctx.lineWidth = 1
|
||||
ctx.fillStyle = dotGradient
|
||||
|
||||
textX = 49
|
||||
text = "#{count}"
|
||||
|
||||
if (count < 10)
|
||||
CanvasUtils.roundRect(ctx, 41 * scale, 1 * scale, 16 * scale, 14 * scale, 7 * scale, true, true)
|
||||
else if (count < 100)
|
||||
CanvasUtils.roundRect(ctx, 37 * scale, 1 * scale, 20 * scale, 14 * scale, 7 * scale, true, true)
|
||||
textX = 46
|
||||
else
|
||||
CanvasUtils.roundRect(ctx, 33 * scale, 1 * scale, 25 * scale, 14 * scale, 7 * scale, true, true)
|
||||
text = "99+"
|
||||
textX = 46
|
||||
|
||||
# count text
|
||||
ctx.fillStyle = "rgba(255,255,255,0.9)"
|
||||
ctx.font = "#{11 * scale}px sans-serif"
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillText(text, textX * scale, 12 * scale, 30 * scale)
|
||||
|
||||
return DragCanvas
|
||||
|
||||
module.exports = CanvasUtils
|
|
@ -1,9 +1,10 @@
|
|||
React = require 'react'
|
||||
|
||||
###
|
||||
# Images are supposed to by default show a ghost image when dragging and
|
||||
# dropping. Unfortunatley this does not work in Electron. Since we're a
|
||||
# desktop app we don't want all images draggable, but we do want some (like attachments) to be able to be dragged away with a preview image.
|
||||
Public: Images are supposed to by default show a ghost image when dragging and
|
||||
dropping. Unfortunately this does not work in Electron. Since we're a
|
||||
desktop app we don't want all images draggable, but we do want some (like
|
||||
attachments) to be able to be dragged away with a preview image.
|
||||
###
|
||||
class DraggableImg extends React.Component
|
||||
@displayName: 'DraggableImg'
|
||||
|
|
47
src/components/drop-zone.cjsx
Normal file
47
src/components/drop-zone.cjsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
|
||||
class DropZone extends React.Component
|
||||
@propTypes:
|
||||
shouldAcceptDrop: React.PropTypes.func.isRequired
|
||||
onDrop: React.PropTypes.func.isRequired
|
||||
onDragStateChange: React.PropTypes.func
|
||||
|
||||
constructor: ->
|
||||
|
||||
render: ->
|
||||
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
|
||||
<div {...otherProps} onDragEnter={@_onDragEnter} onDragLeave={@_onDragLeave} onDrop={@_onDrop}>
|
||||
{@props.children}
|
||||
</div>
|
||||
|
||||
# We maintain a "dragCounter" because dragEnter and dragLeave events *stack*
|
||||
# when the user moves the item in and out of DOM elements inside of our container.
|
||||
# It's really awful and everyone hates it.
|
||||
#
|
||||
# Alternative solution *maybe* is to set pointer-events:none; during drag.
|
||||
|
||||
_onDragEnter: (e) =>
|
||||
return unless @props.shouldAcceptDrop(e)
|
||||
@_dragCounter ?= 0
|
||||
@_dragCounter += 1
|
||||
if @_dragCounter is 1 and @props.onDragStateChange
|
||||
@props.onDragStateChange(isDropping: true)
|
||||
return
|
||||
|
||||
_onDragLeave: (e) =>
|
||||
return unless @props.shouldAcceptDrop(e)
|
||||
@_dragCounter -= 1
|
||||
if @_dragCounter is 0 and @props.onDragStateChange
|
||||
@props.onDragStateChange(isDropping: false)
|
||||
return
|
||||
|
||||
_onDrop: (e) =>
|
||||
return unless @props.shouldAcceptDrop(e)
|
||||
if @props.onDragStateChange
|
||||
@props.onDragStateChange(isDropping: false)
|
||||
@_dragCounter = 0
|
||||
@props.onDrop(e)
|
||||
return
|
||||
|
||||
module.exports = DropZone
|
|
@ -98,6 +98,8 @@ class MultiselectList extends React.Component
|
|||
className = @props.className
|
||||
className += " ready" if @state.ready
|
||||
|
||||
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
|
||||
|
||||
if @state.dataView and @state.handler
|
||||
className += " " + @state.handler.cssClass()
|
||||
|
||||
|
@ -116,7 +118,7 @@ class MultiselectList extends React.Component
|
|||
visible={@state.ready && @state.dataView.count() is 0}
|
||||
dataView={@state.dataView} />
|
||||
|
||||
<div className={className}>
|
||||
<div className={className} {...otherProps}>
|
||||
<ListTabular
|
||||
ref="list"
|
||||
columns={@state.computedColumns}
|
||||
|
@ -130,7 +132,7 @@ class MultiselectList extends React.Component
|
|||
{emptyElement}
|
||||
</div>
|
||||
else
|
||||
<div className={className}>
|
||||
<div className={className} {...otherProps}>
|
||||
<Spinner visible={@state.ready is false} />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -27,6 +27,10 @@ class CategoryStore extends NylasStore
|
|||
"trash"
|
||||
]
|
||||
|
||||
LockedCategoryNames: [
|
||||
"sent"
|
||||
]
|
||||
|
||||
AllMailName: "all"
|
||||
|
||||
byId: (id) -> @_categoryCache[id]
|
||||
|
|
|
@ -88,6 +88,7 @@ class Toolbar extends React.Component
|
|||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@mounted = true
|
||||
@unlisteners = []
|
||||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
@ -97,6 +98,7 @@ class Toolbar extends React.Component
|
|||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
componentWillUnmount: =>
|
||||
@mounted = false
|
||||
window.removeEventListener("resize", @_onWindowResize)
|
||||
unlistener() for unlistener in @unlisteners
|
||||
|
||||
|
@ -105,7 +107,6 @@ class Toolbar extends React.Component
|
|||
|
||||
componentDidUpdate: =>
|
||||
# Wait for other components that are dirty (the actual columns in the sheet)
|
||||
# to update as well.
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
|
@ -148,6 +149,9 @@ class Toolbar extends React.Component
|
|||
</TimeoutTransitionGroup>
|
||||
|
||||
recomputeLayout: =>
|
||||
# Yes this really happens - do not remove!
|
||||
return unless @mounted
|
||||
|
||||
# Find our item containers that are tied to specific columns
|
||||
columnToolbarEls = React.findDOMNode(@).querySelectorAll('[data-column]')
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
display: inline-block;
|
||||
cursor:default;
|
||||
line-height: 22px;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.mail-label.removable {
|
||||
padding-left:12px;
|
||||
|
|
Loading…
Reference in a new issue