Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add forward and background arrows to canvas to undo/redo #419

Merged
merged 9 commits into from
May 30, 2016
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"react-markdown": "^2.2.0",
"react-modal": "^1.2.1",
"react-redux": "^4.4.5",
"react-tooltip": "^2.0.2",
"redux": "^3.5.2",
"redux-crud": "^0.10.1",
"redux-saga": "^0.10.4",
Expand Down
19 changes: 17 additions & 2 deletions src/components/spaces/denormalized-space-selector.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { createSelector } from 'reselect';
import e from 'gEngine/engine'

const spaceGraphSelector = state => { return _.pick(state, 'spaces', 'metrics', 'guesstimates', 'simulations', 'users', 'organizations', 'userOrganizationMemberships', 'me') }
const spaceSelector = (state, props) => {return state.spaces.find(s => s.id.toString() === props.spaceId.toString())}
function _sameId(idA, idB){
return idA.toString() === idB.toString()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this defensivity here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it for at least one of the IDs, I forgot which.

}

function checkpointMetadata(id, checkpoints) {
let attributes = {head: 0, length: 1}
let spaceCheckpoints = checkpoints.find(i => _sameId(i.spaceId, id))
if (!_.isEmpty(spaceCheckpoints)) {
attributes = {head: spaceCheckpoints.head, length: spaceCheckpoints.checkpoints.length}
}
return attributes
}

const spaceGraphSelector = state => { return _.pick(state, 'spaces', 'metrics', 'guesstimates', 'simulations', 'users', 'organizations', 'userOrganizationMemberships', 'me', 'checkpoints') }
const spaceSelector = (state, props) => {return state.spaces.find(s => _sameId(s.id, props.spaceId))}
const canvasStateSelector = state => {return state.canvasState}
const guesstimateFormSelector = state => {return state.guesstimateForm}

Expand All @@ -17,6 +30,8 @@ export const denormalizedSpaceSelector = createSelector(
if (dSpace) {
dSpace.edges = e.dgraph.dependencyMap(dSpace, guesstimateForm)
dSpace.canvasState = canvasState

dSpace.checkpointMetadata = checkpointMetadata(space.id, graph.checkpoints)
dSpace.metrics = dSpace.metrics.map(s => {
let edges = {}
edges.inputs = dSpace.edges.filter(i => i.output === s.id).map(e => e.input)
Expand Down
19 changes: 19 additions & 0 deletions src/components/spaces/show/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CanvasViewForm from './canvasViewForm'
import DropDown, {DropDownListElement} from 'gComponents/utility/drop-down/index'
import {PrivacyToggle} from './privacy-toggle/index'
import {SpaceName} from './spaceName'
import ReactTooltip from 'react-tooltip'

import e from 'gEngine/engine'

Expand Down Expand Up @@ -50,14 +51,21 @@ const SpaceHeader = ({
onPublicSelect,
onPrivateSelect,
onSaveName,
onUndo,
canUndo,
onRedo,
canRedo
}) => {
let privacy_header = (<span><Icon name='globe'/> Public</span>)
if (isPrivate) {
privacy_header = (<span><Icon name='lock'/> Private</span>)
}
const ReactTooltipParams = {class: 'small-tooltip', delayShow: 0, delayHide: 0, place: 'bottom', effect: 'solid'}

return (
<div className='header'>
<ReactTooltip {...ReactTooltipParams} id='undo-button'>Undo (ctrl-z)</ReactTooltip>
<ReactTooltip {...ReactTooltipParams} id='redo-button'>Redo (ctrl-shift-z)</ReactTooltip>

<div className='header-name'>
<SpaceName
Expand All @@ -68,6 +76,17 @@ const SpaceHeader = ({
</div>

<div className='header-actions'>
{editableByMe &&
<div className='ui buttons tiny'>
<button onClick={onUndo} className={`ui icon button ${canUndo ? '' : 'disabled'}`} data-tip data-for='undo-button'>
<Icon name='undo'/>
</button>
<button onClick={onRedo} className={`ui icon button ${canRedo ? '' : 'disabled'}`} data-tip data-for='redo-button'>
<Icon name='repeat'/>
</button>
</div>
}

<CanvasViewForm/>

{editableByMe &&
Expand Down
35 changes: 27 additions & 8 deletions src/components/spaces/show/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Canvas from 'gComponents/spaces/canvas'
import {denormalizedSpaceSelector} from '../denormalized-space-selector.js'

import * as spaceActions from 'gModules/spaces/actions.js'
import {undo, redo} from 'gModules/checkpoints/actions'

import e from 'gEngine/engine'

Expand Down Expand Up @@ -67,31 +68,41 @@ export default class SpacesShow extends Component {

fetchData() {
if (!this.state.attemptedFetch) {
this.props.dispatch(spaceActions.fetchById(parseInt(this.props.spaceId)))
this.props.dispatch(spaceActions.fetchById(this._id()))
this.setState({attemptedFetch: true})
}
}

onSave() {
this.props.dispatch(spaceActions.update(parseInt(this.props.spaceId)))
this.props.dispatch(spaceActions.update(this._id()))
}

onRedo() {
this.props.dispatch(redo(this._id()))
}

onUndo() {
this.props.dispatch(undo(this._id()))
}

destroy() {
this.props.dispatch(spaceActions.destroy(this.props.denormalizedSpace))
}

onPublicSelect() {
this.props.dispatch(spaceActions.generalUpdate(parseInt(this.props.spaceId), {is_private: false}))
this.props.dispatch(spaceActions.generalUpdate(this._id(), {is_private: false}))
}

onPrivateSelect() {
this.props.dispatch(spaceActions.generalUpdate(parseInt(this.props.spaceId), {is_private: true}))
this.props.dispatch(spaceActions.generalUpdate(this._id(), {is_private: true}))
}

onSaveName(name) {
this.props.dispatch(spaceActions.update(parseInt(this.props.spaceId), {name}))
this.props.dispatch(spaceActions.update(this._id(), {name}))
}

onSaveDescription(description) {
this.props.dispatch(spaceActions.update(parseInt(this.props.spaceId), {description}))
this.props.dispatch(spaceActions.update(this._id(), {description}))
}

hideSidebar() {
Expand All @@ -102,7 +113,11 @@ export default class SpacesShow extends Component {
}

_handleCopy() {
this.props.dispatch(spaceActions.copy(parseInt(this.props.spaceId)))
this.props.dispatch(spaceActions.copy())
}

_id() {
return parseInt(this.props.spaceId)
}

render() {
Expand Down Expand Up @@ -170,7 +185,11 @@ export default class SpacesShow extends Component {
actionState={space.canvasState.actionState}
canBePrivate={canBePrivate}
onPublicSelect={this.onPublicSelect.bind(this)}
onPrivateSelect={this.onPrivateSelect.bind(this)}
onPrivateSelecundot={this.onPrivateSelect.bind(this)}
onUndo={this.onUndo.bind(this)}
onRedo={this.onRedo.bind(this)}
canUndo={space.checkpointMetadata.head !== space.checkpointMetadata.length - 1}
Copy link
Contributor

@mmcdermott mmcdermott May 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your null state in the selector (head: 0, length: 0) sets canUndo to true. Maybe null state should be head: 0 length: 1? As a result, the button ever so briefly flashes as updated before the first checkpoint is registered on a hard refresh of the page.

canRedo={space.checkpointMetadata.head !== 0}
/>
</div>

Expand Down
47 changes: 47 additions & 0 deletions src/components/spaces/show/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,34 @@
float: left;
}

.hero-unit .header-actions .ui.buttons {
float: left;
margin-top: 5px;
margin-right: 8px;
}

.hero-unit .header-actions .ui.buttons .ui.button {
}

.hero-unit .header-actions .ui.buttons.tiny .ui.button {
padding: .4rem;
font-size: 1rem;
opacity: 1 !important;
background-color: rgba(255,255,255,0.55);
color: rgba(7, 67, 105, 0.87);
}

.hero-unit .header-actions .ui.buttons .ui.button:hover {
background-color: rgba(255,255,255,0.7);
color: rgba(7, 67, 105, 0.94);
}

.hero-unit .header-actions .ui.buttons .ui.button.disabled {
background-color: rgba(255,255,255,0.15);
color: rgba(7, 67, 105, 0.4);
pointer-events: all;
}

.hero-unit a.space-header-action {
color: white;
padding: 0.5em 0.95em;
Expand Down Expand Up @@ -206,3 +234,22 @@
.SpaceSidebar .SpaceSidebar-body .ClickToEdit .EditingMode textarea {
min-height: 30em;
}

.small-tooltip.small-tooltip.small-tooltip {
background: rgba(60, 64, 68, 0.95);
opacity: 1;
padding: 4px 13px;
border-radius: 2px;
transition: none;
&.place-bottom {
margin-top: 2px;
}

&.place-bottom:after {
border-bottom: 6px solid rgba(60, 64, 68, 0.95);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
top: -6px;
margin-left: -6px;
}
}