UndoRedoButton now shows

Summary: A notification now appears at the bottom of the window when the user performs an action that can be undone

Test Plan: Tested manually

Reviewers: bengotow

Reviewed By: bengotow

Subscribers: mg

Differential Revision: https://phab.nylas.com/D1820
This commit is contained in:
EthanBlackburn 2015-07-31 13:12:47 -07:00
parent 12a2836ec9
commit 2814a0bc70
11 changed files with 236 additions and 24 deletions

View file

@ -57,6 +57,7 @@ class ThreadList extends React.Component
constructor: (@props) ->
@state =
style: 'unknown'
task: null
componentWillMount: =>
c1 = new ListTabular.Column
@ -169,33 +170,42 @@ class ThreadList extends React.Component
render: =>
if @state.style is 'wide'
<MultiselectList
dataStore={ThreadListStore}
columns={@wideColumns}
commands={@commands}
itemPropsProvider={@itemPropsProvider}
itemHeight={39}
className="thread-list"
scrollTooltipComponent={ThreadListScrollTooltip}
emptyComponent={EmptyState}
onDragStart={@_onDragStart}
onDragEnd={@_onDragEnd}
draggable="true"
collection="thread" />
<div>
<MultiselectList
dataStore={ThreadListStore}
columns={@wideColumns}
commands={@commands}
itemPropsProvider={@itemPropsProvider}
itemHeight={39}
className="thread-list"
scrollTooltipComponent={ThreadListScrollTooltip}
emptyComponent={EmptyState}
onDragStart={@_onDragStart}
onDragEnd={@_onDragEnd}
draggable="true"
collection="thread" />
</div>
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}
emptyComponent={EmptyState}
collection="thread" />
<div>
<MultiselectList
dataStore={ThreadListStore}
columns={@narrowColumns}
commands={@commands}
itemPropsProvider={@itemPropsProvider}
itemHeight={90}
className="thread-list thread-list-narrow"
scrollTooltipComponent={ThreadListScrollTooltip}
emptyComponent={EmptyState}
collection="thread" />
</div>
else
<div></div>
# _renderUndoRedo: =>
# if @state.task
# <UndoRedoComponent task:{null}
# show:{false} />
_threadIdAtPoint: (x, y) ->
item = document.elementFromPoint(event.clientX, event.clientY).closest('.list-item')
return null unless item

View file

@ -0,0 +1,13 @@
{ComponentRegistry, WorkspaceStore} = require 'nylas-exports'
module.exports =
activate: (@state={}) ->
UndoRedoComponent = require "./undo-redo-component"
ComponentRegistry.register UndoRedoComponent,
location: WorkspaceStore.Location.ThreadList
deactivate: ->
ComponentRegistry.unregister UndoRedoComponent
serialize: -> @state

View file

@ -0,0 +1,90 @@
_ = require 'underscore'
path = require 'path'
React = require 'react'
classNames = require 'classnames'
{RetinaImg, TimeoutTransitionGroup} = require 'nylas-component-kit'
{Actions,
Utils,
ComponentRegistry,
UndoRedoStore,
NamespaceStore} = require 'nylas-exports'
class UndoRedoComponent extends React.Component
@displayName: 'UndoRedoComponent'
@propTypes:
task: React.PropTypes.object.isRequired
show: React.PropTypes.bool
constructor: (@props) ->
@state = @_getStateFromStores()
@_timeout = null
_onChange: =>
@setState(@_getStateFromStores(), =>
@_setNewTimeout())
_clearTimeout: =>
clearTimeout(@_timeout)
_setNewTimeout: =>
clearTimeout(@_timeout)
@_timeout = setTimeout (=>
@_hide()
return
), 3000
_getStateFromStores: ->
t = UndoRedoStore.getMostRecentTask()
s = false
if t
s = true
return {show: s, task: t}
componentWillMount: ->
@unsub = UndoRedoStore.listen(@_onChange)
componentWillUnmount: ->
@unsub()
render: =>
items = [].concat(@_renderUndoRedoManager())
names = classNames
"undo-redo-manager": true
<TimeoutTransitionGroup
className={names}
leaveTimeout={450}
enterTimeout={250}
transitionName="undo-redo-item">
{items}
</TimeoutTransitionGroup>
_renderUndoRedoManager: =>
if @state.show
<div className="undo-redo" onMouseEnter={@_clearTimeout} onMouseLeave={@_setNewTimeout}>
<div className="undo-redo-message-wrapper">
<p className="undo-redo-message">{@_formatMessage()}</p>
</div>
<div className="undo-redo-action-wrapper" onClick={@_undoTask}>
<RetinaImg name="undo-icon@2x.png"
mode={RetinaImg.Mode.ContentPreserve}/>
<p className="undo-redo-action-text">Undo</p>
</div>
</div>
else
[]
_undoTask: =>
atom.commands.dispatch(document.querySelector('body'), 'core:undo')
@_hide()
_formatMessage: =>
return @state.task.description()
_hide: =>
@setState({show: false, task: null})
module.exports = UndoRedoComponent

View file

@ -0,0 +1,13 @@
{
"name": "undo-redo",
"version": "0.1.0",
"main": "./lib/main",
"description": "Undo modal button",
"license": "Proprietary",
"private": true,
"engines": {
"atom": "*"
},
"dependencies": {
}
}

View file

@ -0,0 +1,53 @@
@import "ui-variables";
@import "ui-mixins";
.undo-redo{
position: absolute;
height: 45px;
bottom: 10px;
left: 39%;
padding-top: 12px;
opacity: 0.9;
border-radius: @border-radius-base;
background: @gray-darker;
.undo-redo-message-wrapper{
display: inline-block;
margin-left: 15px;
margin-right: 30px;
.undo-redo-message{
color: lighten(@gray-base, 70%);;
}
}
.undo-redo-action-wrapper{
float: right;
display: inline-block;
margin-right: 15px;
.undo-redo-action-text{
margin-left: 5px;
display: inline-block;
color: @white;
}
}
}
.undo-redo-item-enter {
opacity: 0.01;
transition: all .3s ease-out;
}
.undo-redo-item-enter.undo-redo-item-enter-active {
opacity: 1;
}
.undo-redo-item-leave {
opacity: 1;
transition: all .3s ease-in;
}
.undo-redo-item-leave.undo-redo-item-leave-active {
opacity: 0.01;
}

View file

@ -25,11 +25,12 @@ class UndoRedoStore
if task.canBeUndone() and not task.isUndo()
@_redo = []
@_undo.push(task)
@trigger() unless task._isReverting
undo: =>
topTask = @_undo.pop()
return unless topTask
@trigger()
Actions.queueTask(topTask.createUndoTask())
@_redo.push(topTask.createIdenticalTask())
@ -38,6 +39,10 @@ class UndoRedoStore
return unless redoTask
Actions.queueTask(redoTask)
getMostRecentTask: =>
for idx in [@_undo.length-1...-1]
return @_undo[idx] unless @_undo[idx]._isReverting
print: ->
console.log("Undo Stack")
console.log(@_undo)

View file

@ -23,6 +23,19 @@ class ChangeFolderTask extends ChangeCategoryTask
label: -> "Moving to folder…"
description: ->
folderName = @folderOrId.displayName
if @threadIds.length > 0
if @threadIds.length > 1
return "Moving " + @threadIds.length + " threads to \"" + folderName + "\""
return "Moving 1 thread to \"" + folderName + "\""
else if @messageIds.length > 0
if @messageIds.length > 1
return "Moving " + @messageIds.length + "messages to \"" + folderName + "\""
return "Moving 1 message to \"" + folderName + "\""
else
return "Moving objects to \"" + folderName + "\""
collectCategories: ->
if @folderOrId instanceof Folder
return Promise.props

View file

@ -23,6 +23,12 @@ class ChangeLabelsTask extends ChangeCategoryTask
label: -> "Applying labels…"
description: ->
addingMessage = "Adding " + @labelsToAdd.length + " labels"
removingMessage = "Removing " + @labelsToRemove.length + " labels"
return addingMessage + " " + removingMessage
collectCategories: ->
labelOrIdPromiseMapper = (labelOrId) ->
if labelOrId instanceof Label

View file

@ -20,6 +20,10 @@ class UpdateNylasObjectsTask extends Task
endpoint: (obj) ->
inflection.pluralize(obj.constructor.name.toLowerCase())
# OVERRIDE ME
description: ->
'Updating Nylas object'
performLocal: ({reverting}={}) ->
if reverting or @isUndo()
Promise.map @objects, (obj) =>

View file

@ -134,6 +134,11 @@ class Task
throw new Error('Cannot send message to terminated process')
undefined
# Public: Describe the function of the task. Each task should override this
# to explain its individual function
description: ->
''
# Public: Call a function when an event is emitted by the child process
#
# * `eventName` The {String} name of the event to handle.

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B