Replace front-end with React
This commit is contained in:
parent
024b66d4c7
commit
07fbe5ece2
3 changed files with 214 additions and 102 deletions
162
js/main.js
Normal file
162
js/main.js
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/** @jsx React.DOM */
|
||||||
|
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 StoryData = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
if (this.props.data) {
|
||||||
|
return (<div>
|
||||||
|
Assignee: {this.props.data.Assignee}
|
||||||
|
<pre>{this.props.data.content}</pre>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryRow = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
// A little ugly to get a space, but I don't know of any other
|
||||||
|
// way.
|
||||||
|
var state = " " + this.props.story.state;
|
||||||
|
var sdata = null;
|
||||||
|
|
||||||
|
if (this.state.content)
|
||||||
|
sdata = <StoryData data={this.state.content} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<StateIcon state={this.props.story.state} />
|
||||||
|
{state}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a id={this.props.story.id} onClick={this.handleClick}>
|
||||||
|
As a {this.props.story.role}, I
|
||||||
|
{this.props.story.necessity} to
|
||||||
|
{this.props.story.title}
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
{sdata}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {content: null};
|
||||||
|
},
|
||||||
|
handleClick: React.autoBind(function(event) {
|
||||||
|
if (!!this.state.content) {
|
||||||
|
this.setState({content: null});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
$.get('/stories/' + this.props.story.id, null,
|
||||||
|
function (data, textStatus, jqXHR) {
|
||||||
|
self.setState({content: data});
|
||||||
|
}, 'json');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryTable = React.createClass({
|
||||||
|
loadStoriesFromServer: function() {
|
||||||
|
$.ajax({
|
||||||
|
url: this.props.url,
|
||||||
|
mimeType: 'textPlain',
|
||||||
|
success: function(data) {
|
||||||
|
this.setState({data: eval(data)});
|
||||||
|
}.bind(this)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {data: []};
|
||||||
|
},
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.loadStoriesFromServer();
|
||||||
|
setInterval(
|
||||||
|
this.loadStoriesFromServer.bind(this),
|
||||||
|
this.props.pollInterval
|
||||||
|
);
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var storyNodes = this.state.data.map(function (story) {
|
||||||
|
return <StoryRow story={story} />;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<table class="table table-striped">
|
||||||
|
{storyNodes}
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
this.props.onStorySubmit({role: role,
|
||||||
|
necessity: necessity,
|
||||||
|
headline: headline});
|
||||||
|
|
||||||
|
this.refs.role.getDOMNode().value = '';
|
||||||
|
this.refs.necessity.getDOMNode().value = '';
|
||||||
|
this.refs.headline.getDOMNode().value = '';
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="toggle">New story</legend>
|
||||||
|
<div id="new-story">
|
||||||
|
<div class="input-prepend input-append">
|
||||||
|
<span class="add-on">As a </span>
|
||||||
|
<input type="text" class="input-medium" ref="role" />
|
||||||
|
<span class="add-on"> I </span>
|
||||||
|
<input type="text" class="input-mini" ref="necessity" />
|
||||||
|
<span class="add-on"> to </span>
|
||||||
|
<input type="text" class="input-xxlarge" ref="headline" />
|
||||||
|
<button class="btn" type="submit">!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryPage = React.createClass({
|
||||||
|
handleStorySubmit: React.autoBind(function (story) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/stories/new/",
|
||||||
|
type: "POST",
|
||||||
|
data: story,
|
||||||
|
dataType: 'json',
|
||||||
|
mimeType: 'textPlain'
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StoryTable url="/stories/" pollInterval={5000} />
|
||||||
|
<StoryForm onStorySubmit={this.handleStorySubmit} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
React.renderComponent(
|
||||||
|
<StoryPage />,
|
||||||
|
document.getElementById('content')
|
||||||
|
);
|
|
@ -1,24 +0,0 @@
|
||||||
(function ($) {
|
|
||||||
$(document).ready(function () {
|
|
||||||
$(".hide").hide();
|
|
||||||
$(".toggle").click(function () {
|
|
||||||
$(document.getElementById($(this).data("show"))).toggle();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})(jQuery);
|
|
||||||
|
|
||||||
function get_story_info(element) {
|
|
||||||
var id = element.id;
|
|
||||||
var data_element = $(element).parent().find(".data");
|
|
||||||
|
|
||||||
if (data_element.length > 0)
|
|
||||||
data_element.remove();
|
|
||||||
else
|
|
||||||
$.get('/stories/' + id, null,
|
|
||||||
function (data, textStatus, jqXHR) {
|
|
||||||
$(element).after("<div class=\"data\">" +
|
|
||||||
"Assignee: " + data.Assignee +
|
|
||||||
"<pre>" + data.content + "</pre>" +
|
|
||||||
"</div>");
|
|
||||||
}, 'json');
|
|
||||||
}
|
|
108
scrumelo.el
108
scrumelo.el
|
@ -1,4 +1,4 @@
|
||||||
;;; scrumelo.el --- Scrum with elnode and org-mode
|
;;; scrumelo.el --- Scrum with elnode and org-mode -*- lexical-binding: t -*-
|
||||||
|
|
||||||
;; Copyright (C) 2013 Tom Willemse
|
;; Copyright (C) 2013 Tom Willemse
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
;; A scrum web app.
|
;; A scrum web app.
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
(require 'elnode)
|
(require 'elnode)
|
||||||
(require 'esxml)
|
(require 'esxml)
|
||||||
(require 'org)
|
(require 'org)
|
||||||
|
@ -40,13 +41,21 @@
|
||||||
"http://code.jquery.com/jquery-2.0.0.min.js"
|
"http://code.jquery.com/jquery-2.0.0.min.js"
|
||||||
"The location of the jQuery JS file.")
|
"The location of the jQuery JS file.")
|
||||||
|
|
||||||
|
(defvar scrumelo-react-js-location
|
||||||
|
"http://cdnjs.cloudflare.com/ajax/libs/react/0.3.2/react.min.js"
|
||||||
|
"The location of the React JS file.")
|
||||||
|
|
||||||
|
(defvar scrumelo-jsxtransformer-js-location
|
||||||
|
"http://cdnjs.cloudflare.com/ajax/libs/react/0.3.2/JSXTransformer.js"
|
||||||
|
"The location of the JSX Transformer JS file.")
|
||||||
|
|
||||||
(defmacro with-scrumelo-http-params (params httpcon &rest body)
|
(defmacro with-scrumelo-http-params (params httpcon &rest body)
|
||||||
"Bind parameters PARAMS from HTTPCON and execute BODY."
|
"Bind parameters PARAMS from HTTPCON and execute BODY."
|
||||||
|
(declare (indent 2))
|
||||||
`(let (,@(mapcar (lambda (p)
|
`(let (,@(mapcar (lambda (p)
|
||||||
`(,p (elnode-http-param ,httpcon ,(symbol-name p))))
|
`(,p (elnode-http-param ,httpcon ,(symbol-name p))))
|
||||||
params))
|
params))
|
||||||
,@body))
|
,@body))
|
||||||
(put 'with-scrumelo-http-params 'lisp-indent-function 2)
|
|
||||||
|
|
||||||
(defun scrumelo--css (href)
|
(defun scrumelo--css (href)
|
||||||
"Return a link pointing to HREF."
|
"Return a link pointing to HREF."
|
||||||
|
@ -66,70 +75,12 @@
|
||||||
"Return a list of all required JS files."
|
"Return a list of all required JS files."
|
||||||
(list (scrumelo--js scrumelo-bootstrap-js-location)
|
(list (scrumelo--js scrumelo-bootstrap-js-location)
|
||||||
(scrumelo--js scrumelo-jquery-js-location)
|
(scrumelo--js scrumelo-jquery-js-location)
|
||||||
|
(scrumelo--js scrumelo-react-js-location)
|
||||||
|
(scrumelo--js scrumelo-jsxtransformer-js-location)
|
||||||
(scrumelo--js "js/scrumelo.js")))
|
(scrumelo--js "js/scrumelo.js")))
|
||||||
|
|
||||||
(defun scrumelo--story ()
|
|
||||||
"Return a description of the current org heading as a scrum story."
|
|
||||||
(format "As a %s, I %s to %s" (org-entry-get (point) "Role")
|
|
||||||
(org-entry-get (point) "Necessity")
|
|
||||||
(nth 4 (org-heading-components))))
|
|
||||||
|
|
||||||
(defun scrumelo--task-state-class (state)
|
|
||||||
"Return the correct icon class for STATE."
|
|
||||||
(cdr (assoc state '(("TODO" . "icon-check-empty")
|
|
||||||
("DOING" . "icon-sign-blank")
|
|
||||||
("DONE" . "icon-check")))))
|
|
||||||
|
|
||||||
(defun scrumelo--story-row ()
|
|
||||||
"Return a table row for the current org headline."
|
|
||||||
(let ((state (org-entry-get (point) "TODO")))
|
|
||||||
`(tr (td (i (@ (class ,(scrumelo--task-state-class state))) "")
|
|
||||||
" " ,state)
|
|
||||||
(td (a (@ (id ,(org-id-get))
|
|
||||||
(onclick "return get_story_info(this)"))
|
|
||||||
,(scrumelo--story))))))
|
|
||||||
|
|
||||||
(defun scrumelo--maybe-story-row ()
|
|
||||||
"If looking at a top level heading, return a table row for it."
|
|
||||||
(when (= (car (org-heading-components)) 1)
|
|
||||||
(scrumelo--story-row)))
|
|
||||||
|
|
||||||
(defun scrumelo--inner-story-table (buffer)
|
|
||||||
"Return the inner part of the story table for BUFFER."
|
|
||||||
(with-current-buffer buffer
|
|
||||||
(delq nil (org-map-entries
|
|
||||||
'scrumelo--maybe-story-row nil nil 'comment))))
|
|
||||||
|
|
||||||
(defun scrumelo--story-table (buffer)
|
|
||||||
"Return the story table for BUFFER."
|
|
||||||
`(table (@ (class "table table-striped"))
|
|
||||||
,@(scrumelo--inner-story-table buffer)))
|
|
||||||
|
|
||||||
(defun scrumelo--new-story-form ()
|
|
||||||
"Create a form for adding new stories."
|
|
||||||
`(form (@ (method "POST")
|
|
||||||
(action "/stories/new/"))
|
|
||||||
(fieldset
|
|
||||||
(legend (@ (class "toggle")
|
|
||||||
(data-show "new-story")) "New story")
|
|
||||||
(div (@ (id "new-story")
|
|
||||||
(class "hide")
|
|
||||||
(style "text-align: center;"))
|
|
||||||
(div (@ (class "input-prepend input-append"))
|
|
||||||
(span (@ (class "add-on")) "As a ")
|
|
||||||
(input (@ (class "input-medium") (type "text")
|
|
||||||
(name "role")))
|
|
||||||
(span (@ (class "add-on")) " I ")
|
|
||||||
(input (@ (class "input-mini") (type "text")
|
|
||||||
(name "necessity")))
|
|
||||||
(span (@ (class "add-on")) " to ")
|
|
||||||
(input (@ (class "input-xxlarge") (type "text")
|
|
||||||
(name "headline")))
|
|
||||||
(button (@ (class "btn") (type "submit")) "!"))))))
|
|
||||||
|
|
||||||
(defun scrumelo-backlog-page (httpcon)
|
(defun scrumelo-backlog-page (httpcon)
|
||||||
"Send the backlog overview over HTTPCON."
|
"Send the backlog overview over HTTPCON."
|
||||||
(let ((buffer (find-file-noselect scrumelo-project-file)))
|
|
||||||
(elnode-http-start httpcon 200 '("Content-Type" . "text/html"))
|
(elnode-http-start httpcon 200 '("Content-Type" . "text/html"))
|
||||||
(elnode-http-return
|
(elnode-http-return
|
||||||
httpcon
|
httpcon
|
||||||
|
@ -141,8 +92,9 @@
|
||||||
,@(scrumelo--js-list))
|
,@(scrumelo--js-list))
|
||||||
(body
|
(body
|
||||||
(div (@ (class "container"))
|
(div (@ (class "container"))
|
||||||
,(scrumelo--story-table buffer)
|
(h1 "Backlog")
|
||||||
,(scrumelo--new-story-form)))))))))
|
(div (@ (id "content")) "")
|
||||||
|
(script (@ (type "text/jsx") (src "js/main.js")) ""))))))))
|
||||||
|
|
||||||
(defun scrumelo-new-story (httpcon)
|
(defun scrumelo-new-story (httpcon)
|
||||||
"Parse data from HTTPCON and write a new scrum story using it."
|
"Parse data from HTTPCON and write a new scrum story using it."
|
||||||
|
@ -176,17 +128,39 @@
|
||||||
(org-end-of-meta-data-and-drawers)
|
(org-end-of-meta-data-and-drawers)
|
||||||
(org-entry-end-position))))))))
|
(org-entry-end-position))))))))
|
||||||
|
|
||||||
|
(defun scrumelo--org-entry-to-list ()
|
||||||
|
"Turn an org-entry to json."
|
||||||
|
(let ((components (org-heading-components)))
|
||||||
|
(when (= (car components) 1)
|
||||||
|
`((:id . ,(org-id-get))
|
||||||
|
(:state . ,(org-entry-get (point) "TODO"))
|
||||||
|
(:role . ,(org-entry-get (point) "Role"))
|
||||||
|
(:necessity . ,(org-entry-get (point) "Necessity"))
|
||||||
|
(:title . ,(nth 4 components))))))
|
||||||
|
|
||||||
|
(defun scrumelo-main-json (request)
|
||||||
|
"Respond to REQUEST with the json info for the main page."
|
||||||
|
(let ((buffer (find-file-noselect scrumelo-project-file)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(scrumelo--send-json
|
||||||
|
request (cl-map 'vector #'identity
|
||||||
|
(delq nil
|
||||||
|
(org-map-entries
|
||||||
|
#'scrumelo--org-entry-to-list
|
||||||
|
nil nil 'comment)))))))
|
||||||
|
|
||||||
(defun scrumelo-handler (httpcon)
|
(defun scrumelo-handler (httpcon)
|
||||||
"Send the right requests in HTTPCON to the right functions."
|
"Send the right requests in HTTPCON to the right functions."
|
||||||
(elnode-dispatcher
|
(elnode-dispatcher
|
||||||
httpcon
|
httpcon
|
||||||
`(("^/$" . scrumelo-backlog-page)
|
`(("^/$" . scrumelo-backlog-page)
|
||||||
("^/js/scrumelo.js" . ,(elnode-make-send-file
|
("^/js/main.js" . ,(elnode-make-send-file
|
||||||
(concat scrumelo--base-dir "js/scrumelo.js")))
|
(concat scrumelo--base-dir "js/main.js")))
|
||||||
|
("^/stories/$" . scrumelo-main-json)
|
||||||
("^/stories/new/$" . scrumelo-new-story)
|
("^/stories/new/$" . scrumelo-new-story)
|
||||||
("^/stories/\\([a-z0-9:-]+\\)/$" . scrumelo-story-json))))
|
("^/stories/\\([a-z0-9:-]+\\)/$" . scrumelo-story-json))))
|
||||||
|
|
||||||
(elnode-start 'scrumelo-handler :port 8028 :host "localhost")
|
(elnode-start 'scrumelo-handler :port 8028 :host "0.0.0.0")
|
||||||
|
|
||||||
(provide 'scrumelo)
|
(provide 'scrumelo)
|
||||||
;;; scrumelo.el ends here
|
;;; scrumelo.el ends here
|
||||||
|
|
Loading…
Reference in a new issue