diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..a600ccf --- /dev/null +++ b/js/main.js @@ -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 ; + } +}); + +var StoryData = React.createClass({ + render: function() { + if (this.props.data) { + return (
+ Assignee: {this.props.data.Assignee} +
{this.props.data.content}
+
); + } + + 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 = ; + + return ( + + + + {state} + + + + As a {this.props.story.role}, I + {this.props.story.necessity} to + {this.props.story.title} + +
+ {sdata} + + + ); + }, + 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 ; + }); + return ( + + {storyNodes} +
+ ); + } +}); + +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 ( +
+
+ New story +
+
+ As a + + I + + to + + +
+
+
+
+ ); + } +}); + +var StoryPage = React.createClass({ + handleStorySubmit: React.autoBind(function (story) { + $.ajax({ + url: "/stories/new/", + type: "POST", + data: story, + dataType: 'json', + mimeType: 'textPlain' + }); + }), + render: function() { + return ( +
+ + +
+ ); + } +}); + +React.renderComponent( + , + document.getElementById('content') +); diff --git a/js/scrumelo.js b/js/scrumelo.js deleted file mode 100644 index e408aa7..0000000 --- a/js/scrumelo.js +++ /dev/null @@ -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("
" + - "Assignee: " + data.Assignee + - "
" + data.content + "
" + - "
"); - }, 'json'); -} diff --git a/scrumelo.el b/scrumelo.el index 74564f7..a6212c2 100644 --- a/scrumelo.el +++ b/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 @@ -9,6 +9,7 @@ ;; A scrum web app. +(require 'cl-lib) (require 'elnode) (require 'esxml) (require 'org) @@ -40,13 +41,21 @@ "http://code.jquery.com/jquery-2.0.0.min.js" "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) "Bind parameters PARAMS from HTTPCON and execute BODY." + (declare (indent 2)) `(let (,@(mapcar (lambda (p) `(,p (elnode-http-param ,httpcon ,(symbol-name p)))) params)) ,@body)) -(put 'with-scrumelo-http-params 'lisp-indent-function 2) (defun scrumelo--css (href) "Return a link pointing to HREF." @@ -66,83 +75,26 @@ "Return a list of all required JS files." (list (scrumelo--js scrumelo-bootstrap-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"))) -(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) "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-return - httpcon - (concat - "\n" - (sxml-to-xml - `(html (head (title "Scrumelo") - ,@(scrumelo--css-list) - ,@(scrumelo--js-list)) - (body - (div (@ (class "container")) - ,(scrumelo--story-table buffer) - ,(scrumelo--new-story-form))))))))) + (elnode-http-start httpcon 200 '("Content-Type" . "text/html")) + (elnode-http-return + httpcon + (concat + "\n" + (sxml-to-xml + `(html (head (title "Scrumelo") + ,@(scrumelo--css-list) + ,@(scrumelo--js-list)) + (body + (div (@ (class "container")) + (h1 "Backlog") + (div (@ (id "content")) "") + (script (@ (type "text/jsx") (src "js/main.js")) "")))))))) (defun scrumelo-new-story (httpcon) "Parse data from HTTPCON and write a new scrum story using it." @@ -176,17 +128,39 @@ (org-end-of-meta-data-and-drawers) (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) "Send the right requests in HTTPCON to the right functions." (elnode-dispatcher httpcon `(("^/$" . scrumelo-backlog-page) - ("^/js/scrumelo.js" . ,(elnode-make-send-file - (concat scrumelo--base-dir "js/scrumelo.js"))) + ("^/js/main.js" . ,(elnode-make-send-file + (concat scrumelo--base-dir "js/main.js"))) + ("^/stories/$" . scrumelo-main-json) ("^/stories/new/$" . scrumelo-new-story) ("^/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) ;;; scrumelo.el ends here