scrumli/static/js/main.js

469 lines
16 KiB
JavaScript
Raw Normal View History

2013-06-30 22:38:05 +02:00
/** @jsx React.DOM */
2013-07-23 23:40:09 +02:00
/* scrumli --- A simple scrum web application
Copyright (C) 2013 Tom Willemse
scrumli is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
scrumli is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with scrumli. If not, see <http://www.gnu.org/licenses/>. */
2013-06-30 22:38:05 +02:00
var StateIcon = React.createClass({
render: function() {
var icon_names = {"TODO": "icon-check-empty",
"DOING": "icon-sign-blank",
"DONE": "icon-check"};
return <i class={icon_names[this.props.state]}></i>;
}
});
var AssigneeIcon = React.createClass({
render: function() {
var icon;
if (this.props.assignee)
icon = <img src={"https://secure.gravatar.com/avatar/" +
this.props.md5 + '?s=24'}
title={this.props.assignee}
alt={this.props.assignee} />;
else
icon = <i title="Unknown"
class="icon-question icon-border"></i>;
return icon;
}
});
2013-07-04 21:51:31 +02:00
var StoryTaskRow = React.createClass({
2013-07-04 23:55:43 +02:00
changeState: React.autoBind(function(event) {
$.post("/tasks/state", {'id': this.props.task.id})
.done(function (data, textStatus, jqXHR) {
2013-07-07 23:46:15 +02:00
if (data.status == "ok")
this.setState({state: data.state});
}.bind(this));
2013-07-04 23:55:43 +02:00
}),
2013-07-07 23:46:15 +02:00
getInitialState: function () {
return {state: this.props.task.state};
},
2013-07-04 23:55:43 +02:00
moveUp: React.autoBind(function(event) {
$.post("/tasks/up", {'id': this.props.task.id})
.done(function (data) {
if (data.status == "ok")
this.props.onMoved(1);
}.bind(this));
2013-07-04 23:55:43 +02:00
}),
moveDown: React.autoBind(function(event) {
$.post("/tasks/down", {'id': this.props.task.id})
.done(function (data) {
if (data.status == "ok")
this.props.onMoved(-1);
}.bind(this));
2013-07-04 23:55:43 +02:00
}),
handleAssigneeClick: React.autoBind(function(event) {
this.props.onAssigneeClicked({url: "/tasks/assignee",
id: this.props.task.id,
assignee: this.props.task.assignee,
md5: this.props.task.md5});
}),
2013-07-04 21:51:31 +02:00
render: function() {
return (
<tr>
<td class="span1">
2013-07-13 21:23:46 +02:00
<i class="icon-arrow-up clickable"
onClick={this.moveUp}></i>
<i class="icon-arrow-down clickable"
onClick={this.moveDown}></i>
2013-07-04 21:51:31 +02:00
</td>
<td class="span1">
<button onClick={this.handleAssigneeClick}
class="nothing">
<AssigneeIcon assignee={this.props.task.assignee}
md5={this.props.task.md5} />
</button>
</td>
2013-07-04 21:51:31 +02:00
<td class="span2">
2013-07-13 21:23:46 +02:00
<span onClick={this.changeState} class="clickable">
2013-07-07 23:46:15 +02:00
<StateIcon state={this.state.state} /> {" "}
{this.state.state}
2013-07-04 21:51:31 +02:00
</span>
</td>
<td>
{this.props.task.description}
</td>
</tr>
);
}
});
var StoryTaskTable = React.createClass({
render: function() {
var taskNodes = this.props.tasks.map(function (task) {
return <StoryTaskRow task={task}
onMoved={this.props.onTaskMoved}
onAssigneeClicked={this.props.onAssigneeClicked} />;
2013-07-05 00:02:10 +02:00
}.bind(this));
2013-07-04 21:51:31 +02:00
return (
<table class="table table-striped">
{taskNodes}
</table>
);
}
});
var StoryTaskForm = React.createClass({
handleSubmit: React.autoBind(function() {
var text = this.refs.text.getDOMNode().value.trim();
this.props.onTaskSubmit({description: text});
this.refs.text.getDOMNode().value = '';
return false;
}),
render: function() {
return (
<form onSubmit={this.handleSubmit}>
<fieldset>
<legend>New task</legend>
<div class="input-append">
<input type="text" ref="text" class="input-medium" />
<button type="submit" class="btn btn-primary">
Send
</button>
</div>
</fieldset>
</form>
);
}
});
2013-06-30 22:38:05 +02:00
var StoryData = React.createClass({
2013-07-04 21:51:31 +02:00
handleTaskSubmit: React.autoBind(function (task) {
2013-07-13 19:57:22 +02:00
task.storyId = this.state.data.id;
$.post("/stories/tasks/new", task)
.done(function(data) {
if (data.status == "ok")
this.loadStoryFromServer();
}.bind(this));
2013-07-04 21:51:31 +02:00
}),
loadStoryFromServer: function() {
$.get("/stories/" + this.state.data.id)
.done(this.setData.bind(this));
},
componentWillMount: function() {
setInterval(
this.loadStoryFromServer.bind(this),
this.props.pollInterval
);
},
2013-07-13 19:57:22 +02:00
getInitialState: function() {
return {data: null};
},
setData: function(data) {
this.setState({data: null});
2013-07-13 19:57:22 +02:00
this.setState({data: data});
},
handleTaskMoved: React.autoBind(function(direction) {
this.loadStoryFromServer();
}),
2013-06-30 22:38:05 +02:00
render: function() {
2013-07-13 19:57:22 +02:00
if (this.state.data) {
2013-06-30 22:38:05 +02:00
return (<div>
2013-07-13 19:57:22 +02:00
<h1>{this.state.data.title}</h1>
2013-07-16 20:53:18 +02:00
<div class="well normalText">
2013-07-13 19:57:22 +02:00
{this.state.data.content}
</div>
<StoryTaskTable tasks={this.state.data.tasks || []}
onTaskMoved={this.handleTaskMoved}
onAssigneeClicked={this.props.onAssigneeClicked} />
2013-07-13 19:57:22 +02:00
<StoryTaskForm onTaskSubmit={this.handleTaskSubmit} />
2013-06-30 22:38:05 +02:00
</div>);
}
2013-07-13 19:57:22 +02:00
return <div></div>;
2013-06-30 22:38:05 +02:00
}
});
var StoryRow = React.createClass({
handleAssigneeClick: React.autoBind(function(event) {
this.props.onAssigneeClicked({url: "/story/assignee",
id: this.props.story.id,
assignee: this.props.story.assignee,
md5: this.props.story.md5});
}),
2013-06-30 22:38:05 +02:00
render: function() {
return (
<tr>
2013-07-04 23:45:07 +02:00
<td class="span1">
2013-07-13 21:23:46 +02:00
<i class="icon-arrow-up clickable"
onClick={this.moveUp}></i>
<i class="icon-arrow-down clickable"
onClick={this.moveDown}></i>
2013-06-30 22:38:05 +02:00
</td>
<td class="span1">
<button onClick={this.handleAssigneeClick}
class="nothing">
<AssigneeIcon assignee={this.props.story.assignee}
md5={this.props.story.md5} />
</button>
</td>
2013-07-04 23:45:07 +02:00
<td class="span2">
2013-07-13 21:23:46 +02:00
<span onClick={this.changeState} class="clickable">
2013-07-07 23:46:15 +02:00
<StateIcon state={this.state.state} /> {" "}
{this.state.state}
2013-06-30 22:38:05 +02:00
</span>
</td>
<td>
2013-07-13 21:23:46 +02:00
<a onClick={this.handleClick} class="clickable">
2013-06-30 22:38:05 +02:00
As a {this.props.story.role}, I
{this.props.story.necessity} to
{this.props.story.title}
</a>
</td>
</tr>
);
},
getInitialState: function() {
return {state: this.props.story.state,
content: null};
},
handleClick: React.autoBind(function(event) {
2013-07-13 19:57:22 +02:00
this.props.onTitleClicked(this.props.story.id);
2013-06-30 22:38:05 +02:00
}),
changeState: React.autoBind(function(event) {
$.post("/stories/state", {'id': this.props.story.id})
.done(function(data, textStatus, jqXHR) {
2013-07-07 23:46:15 +02:00
if (data.status == "ok")
this.setState({state: data.state});
}.bind(this));
2013-06-30 22:38:05 +02:00
}),
moveUp: React.autoBind(function(event) {
$.post("/stories/up", {'id': this.props.story.id})
.done(function (data, textStatus, jqXHR) {
2013-07-07 23:46:15 +02:00
if (data.status == "ok")
this.props.onMoved(1);
}.bind(this));
2013-06-30 22:38:05 +02:00
}),
moveDown: React.autoBind(function(event) {
$.post("/stories/down", {'id': this.props.story.id})
.done(function (data) {
if (data.status == "ok")
this.props.onMoved(-1);
}.bind(this));
2013-06-30 22:38:05 +02:00
})
});
var StoryTable = React.createClass({
handleMoved: React.autoBind(function(direction) {
2013-07-07 23:46:15 +02:00
this.props.onStoryMoved(direction);
2013-06-30 22:38:05 +02:00
}),
2013-07-13 19:57:22 +02:00
handleSelected: React.autoBind(function(storyId) {
this.props.onStorySelected(storyId);
}),
2013-06-30 22:38:05 +02:00
render: function() {
var storyNodes = this.props.data.map(function (story) {
2013-07-13 19:57:22 +02:00
return <StoryRow story={story} onMoved={this.handleMoved}
onTitleClicked={this.handleSelected}
onAssigneeClicked={this.props.onAssigneeClicked} />;
2013-06-30 22:38:05 +02:00
}.bind(this));
return (
<table class="table table-striped">
{storyNodes}
</table>
);
}
});
var AssignmentForm = React.createClass({
handleChanged: React.autoBind(function() {
var assignee = this.refs.assignee.getDOMNode().value;
$.post(this.state.url,
{id: this.state.id, assignee: assignee})
.done(function (data) {
if (data.status == "ok") {
this.refs.assignee.getDOMNode().value = '';
$(".assignModal").modal('hide');
}
}.bind(this));
}),
setInfo: React.autoBind(function(info) {
this.setState(info);
}),
getInitialState: function () {
return {id: 0, assignee: "", url: "", md5: ""};
},
render: function() {
return (
<div class="assignModal modal fade hide">
<div class="modal-header">
<button type="button" class="close"
data-dismiss="modal">
&times;
</button>
<h3 id="assignModalLabel">Assign</h3>
</div>
<div class="modal-body">
<AssigneeIcon assignee={this.state.assignee}
md5={this.state.md5} /> {" "}
<input type="text" ref="assignee"
value={this.state.assignee}
onChange={this.handleChanged} />
</div>
<div class="modal-footer">
</div>
</div>
);
}
});
2013-06-30 22:38:05 +02:00
var StoryForm = React.createClass({
handleSubmit: React.autoBind(function() {
var role = this.refs.role.getDOMNode().value.trim();
var necessity = this.refs.necessity.getDOMNode().value.trim();
var headline = this.refs.headline.getDOMNode().value.trim();
var content = this.refs.content.getDOMNode().value.trim();
2013-07-24 20:45:43 +02:00
$(".newTaskModal").modal('hide');
2013-06-30 22:38:05 +02:00
this.props.onStorySubmit({role: role,
necessity: necessity,
headline: headline,
content: content});
this.refs.role.getDOMNode().value = '';
this.refs.necessity.getDOMNode().value = '';
this.refs.headline.getDOMNode().value = '';
this.refs.content.getDOMNode().value = '';
return false;
}),
render: function() {
return (
2013-07-24 20:45:43 +02:00
<div class="newTaskModal modal fade hide">
2013-07-15 19:51:45 +02:00
<form onSubmit={this.handleSubmit} class="form-horizontal">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
&times;
</button>
2013-07-24 20:45:43 +02:00
<h3 id="newTaskModalLabel">New story</h3>
2013-07-15 19:51:45 +02:00
</div>
<div class="modal-body">
2013-06-30 22:38:05 +02:00
<div id="new-story">
2013-07-15 19:51:45 +02:00
<div class="control-group">
<label class="control-label">As a</label>
<div class="controls">
<input type="text" ref="role" placeholder="person" />
</div>
</div>
<div class="control-group">
<label class="control-label">I</label>
<div class="controls">
<input type="text" ref="necessity"
placeholder="would like" />
</div>
</div>
<div class="control-group">
<label class="control-label">to</label>
<div class="controls">
<input type="text" ref="headline"
placeholder="fill in this form..." />
</div>
</div>
<div class="control-group">
<div class="controls">
<textarea ref="content"></textarea>
</div>
2013-06-30 22:38:05 +02:00
</div>
</div>
2013-07-15 19:51:45 +02:00
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">
Close
</button>
<button class="btn btn-primary" type="submit">Save</button>
</div>
2013-06-30 22:38:05 +02:00
</form>
2013-07-15 19:51:45 +02:00
</div>
2013-06-30 22:38:05 +02:00
);
}
});
var StoryPage = React.createClass({
loadStoriesFromServer: function() {
$.get(this.props.url)
.done(function(data) {
this.setState({data: []});
this.setState({data: data});
}.bind(this));
},
getInitialState: function() {
return {data: []};
},
componentWillMount: function() {
this.loadStoriesFromServer();
setInterval(
this.loadStoriesFromServer.bind(this),
this.props.pollInterval
);
},
2013-07-07 23:46:15 +02:00
handleStoryMoved: React.autoBind(function (direction) {
this.loadStoriesFromServer();
}),
2013-06-30 22:38:05 +02:00
handleStorySubmit: React.autoBind(function (story) {
$.post("/stories/new", story)
.done(function (data, textStatus, jqXHR) {
if (data.status == "ok")
this.loadStoriesFromServer();
}.bind(this))
.fail(function (jqXHR, textStatus, errorThrown) {
alert("error: " + errorThrown);
}.bind(this));
2013-06-30 22:38:05 +02:00
}),
2013-07-13 19:57:22 +02:00
handleStorySelected: React.autoBind(function (storyId) {
$.get('/stories/' + storyId)
.done(function (data, textStatus, jqXHR) {
this.refs.data.setData(data);
}.bind(this), 'json');
}),
handleAssigneeClicked: React.autoBind(function (info) {
var form = this.refs.assignmentForm;
form.setInfo(info);
$(".assignModal").modal();
}),
2013-06-30 22:38:05 +02:00
render: function() {
return (
2013-07-13 19:57:22 +02:00
<div class="row">
<div class="span6">
<StoryTable data={this.state.data}
onStoryMoved={this.handleStoryMoved}
onStorySelected={this.handleStorySelected}
onAssigneeClicked={this.handleAssigneeClicked} />
2013-07-13 19:57:22 +02:00
<StoryForm onStorySubmit={this.handleStorySubmit} />
<AssignmentForm ref="assignmentForm" />
2013-07-13 19:57:22 +02:00
</div>
<div class="span6">
<StoryData ref="data"
pollInterval={this.props.pollInterval}
onAssigneeClicked={this.handleAssigneeClicked} />
2013-07-13 19:57:22 +02:00
</div>
2013-06-30 22:38:05 +02:00
</div>
);
}
});
React.renderComponent(
<StoryPage url="/stories" pollInterval={5000} />,
2013-06-30 22:38:05 +02:00
document.getElementById('content')
);