diff --git a/Gemfile b/Gemfile
index 6b5b088..8978197 100644
--- a/Gemfile
+++ b/Gemfile
@@ -24,7 +24,9 @@ gem 'jbuilder', '~> 1.2'
gem 'jquery-rails'
gem 'rack-timeout'
gem 'rails', '~> 4.2.0'
+gem 'react-rails', '~> 1.0.0.pre', github: 'reactjs/react-rails'
gem 'sass-rails'
+gem 'sprockets-es6'
gem 'thin'
gem 'turbolinks'
gem 'uglifier', '>= 1.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index d9af6c2..ffce05b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,14 @@
+GIT
+ remote: git://github.com/reactjs/react-rails.git
+ revision: b6538ae1b11f7da82a7186929cedc0dcbba10ae3
+ specs:
+ react-rails (1.0.0.pre)
+ coffee-script-source (~> 1.9)
+ connection_pool
+ execjs
+ rails (>= 3.1)
+ react-source (~> 0.12)
+
GEM
remote: https://rubygems.org/
specs:
@@ -38,6 +49,10 @@ GEM
tzinfo (~> 1.1)
addressable (2.3.7)
arel (6.0.0)
+ babel-source (5.1.9)
+ babel-transpiler (0.7.0)
+ babel-source (>= 4.0, < 6)
+ execjs (~> 2.0)
bcrypt (3.1.10)
builder (3.2.2)
coderay (1.1.0)
@@ -47,7 +62,8 @@ GEM
coffee-script (2.3.0)
coffee-script-source
execjs
- coffee-script-source (1.9.0)
+ coffee-script-source (1.9.1)
+ connection_pool (2.1.3)
daemons (1.1.9)
decent_exposure (2.3.2)
decent_generators (0.0.3)
@@ -72,7 +88,7 @@ GEM
dotenv (1.0.2)
erubis (2.7.0)
eventmachine (1.0.7)
- execjs (2.3.0)
+ execjs (2.4.0)
factory_girl (4.5.0)
activesupport (>= 3.0.0)
faraday (0.9.1)
@@ -81,7 +97,7 @@ GEM
foreman (0.77.0)
dotenv (~> 1.0.2)
thor (~> 0.19.1)
- globalid (0.3.0)
+ globalid (0.3.3)
activesupport (>= 4.1.0)
haml (4.0.6)
tilt
@@ -92,7 +108,6 @@ GEM
html2haml (>= 1.0.1)
railties (>= 4.0.1)
hashie (3.4.0)
- hike (1.2.3)
html2haml (2.0.0)
erubis (~> 2.7.0)
haml (~> 4.0.0)
@@ -117,7 +132,7 @@ GEM
mime-types (2.4.3)
mini_portile (0.6.2)
minitest (5.5.1)
- multi_json (1.10.1)
+ multi_json (1.11.0)
multi_xml (0.5.5)
multipart-post (2.0.0)
newrelic_rpm (3.10.0.279)
@@ -173,7 +188,7 @@ GEM
activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6.0)
rails-deprecated_sanitizer (>= 1.0.1)
- rails-html-sanitizer (1.0.1)
+ rails-html-sanitizer (1.0.2)
loofah (~> 2.0)
railties (4.2.0)
actionpack (= 4.2.0)
@@ -182,6 +197,7 @@ GEM
thor (>= 0.18.1, < 2.0)
raindrops (0.13.0)
rake (10.4.2)
+ react-source (0.13.0)
responders (2.1.0)
railties (>= 4.2.0, < 5)
rspec (3.2.0)
@@ -225,11 +241,11 @@ GEM
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
slop (3.6.0)
- sprockets (2.12.3)
- hike (~> 1.2)
- multi_json (~> 1.0)
+ sprockets (3.0.1)
rack (~> 1.0)
- tilt (~> 1.1, != 1.3.0)
+ sprockets-es6 (0.6.0)
+ babel-transpiler
+ sprockets (~> 3.0.0.beta)
sprockets-rails (2.2.4)
actionpack (>= 3.0)
activesupport (>= 3.0)
@@ -239,7 +255,7 @@ GEM
eventmachine (~> 1.0)
rack (~> 1.0)
thor (0.19.1)
- thread_safe (0.3.4)
+ thread_safe (0.3.5)
tilt (1.4.1)
turbolinks (2.5.3)
coffee-rails
@@ -288,10 +304,12 @@ DEPENDENCIES
pry-rails
rack-timeout
rails (~> 4.2.0)
+ react-rails (~> 1.0.0.pre)!
rspec
rspec-rails
sass-rails
shoulda-matchers
+ sprockets-es6
thin
turbolinks
twitter-bootstrap-rails
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 37fbb8a..5f83ab8 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -16,4 +16,7 @@
//= require turbolinks
//= require mousetrap-1.4.6
//= require fastclick-1.0.6
+//= require react
+//= require components
+//= require react_ujs
//= require_tree .
diff --git a/app/assets/javascripts/checklists.js b/app/assets/javascripts/checklists.js
deleted file mode 100644
index 1be3ad4..0000000
--- a/app/assets/javascripts/checklists.js
+++ /dev/null
@@ -1,27 +0,0 @@
-$(document).on('ajax:success', '.destroy-checklist-item', function() {
- $(this).closest('.row').slideUp('slow', function() { $(this).remove() })
-})
-
-$(document).on('ajax:success', '.new_checklist_item', function(e, data) {
- var $form = $(this)
- $form[0].reset()
- $(data).insertBefore($form.closest('.row'))
-}).on('ajax:error', '.new_checklist_item, .edit_checklist_item', function(e, xhr) {
- alert(xhr.responseText)
-})
-
-$(document).on('click', '.edit_checklist_item:not([data-edit-mode]) .checklist-item-name', function() {
- var $form = $(this).closest('.edit_checklist_item')
- $form.attr('data-edit-mode', true).
- find('input[type="text"]').focus().end().
- closest('.checklist-items').find('.edit_checklist_item').not($form).removeAttr('data-edit-mode')
- return false
-}).on('click', function(e) {
- if (!$(e.target).is('.edit_checklist_item input[type="text"]')) {
- $('.edit_checklist_item').removeAttr('data-edit-mode')
- }
-})
-
-$(document).on('ajax:success', '.edit_checklist_item', function(e, data) {
- $(this).closest('.row').replaceWith(data)
-})
diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js
new file mode 100644
index 0000000..9ce7a4f
--- /dev/null
+++ b/app/assets/javascripts/components.js
@@ -0,0 +1 @@
+//= require_tree ./components
diff --git a/app/assets/javascripts/components/.gitkeep b/app/assets/javascripts/components/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/assets/javascripts/components/checklist.js.es6.jsx b/app/assets/javascripts/components/checklist.js.es6.jsx
new file mode 100644
index 0000000..db00481
--- /dev/null
+++ b/app/assets/javascripts/components/checklist.js.es6.jsx
@@ -0,0 +1,18 @@
+var Checklist = React.createClass({
+ render() {
+ return (
+
+
+
{this.props.name}
+
+
+
+ );
+ }
+})
+
+window.Checklist = Checklist
diff --git a/app/assets/javascripts/components/checklist_item.js.jsx b/app/assets/javascripts/components/checklist_item.js.jsx
new file mode 100644
index 0000000..4744a21
--- /dev/null
+++ b/app/assets/javascripts/components/checklist_item.js.jsx
@@ -0,0 +1,66 @@
+window.ChecklistItem = React.createClass({
+ removeLink: function() { return React.findDOMNode(this.refs.removeLink); },
+ row: function() { return React.findDOMNode(this.refs.row); },
+
+ componentDidMount: function() {
+ var self = this;
+
+ $(this.removeLink()).on('ajax:success.ChecklistItem', function() {
+ $(self.row()).slideUp('slow', function() { self.removeItem(this.state.item.id) })
+ });
+ },
+
+ componentWillUnmount: function() {
+ $(this.removeLink()).off('ajax:success.ChecklistItem')
+ },
+
+ formAjaxSuccess: function(newItem) {
+ this.toggleEditMode();
+ this.setState({item: newItem});
+ },
+
+ getInitialState: function() {
+ return { item: this.props.item };
+ },
+
+ currentlyInEditMode: function() {
+ return this.props.editModeIdx === this.props.idx;
+ },
+
+ handleRowClick: function(e) {
+ if(!$(e.target).is('input, a')) {
+ this.toggleEditMode();
+ }
+ },
+
+ toggleEditMode: function() {
+ this.props.updateEditModeIdx(this.currentlyInEditMode() ? -1 : this.props.idx);
+ },
+
+ render: function() {
+ var name = (
+
+
+
+ {this.state.item.name}
+
+
+
+
+
+ );
+
+ var form = (
+
+ )
+
+ return (
+
+ {this.currentlyInEditMode() ? form : name}
+
+
+ );
+ }
+})
diff --git a/app/assets/javascripts/components/checklist_item_form.js.jsx b/app/assets/javascripts/components/checklist_item_form.js.jsx
new file mode 100644
index 0000000..f01a3ba
--- /dev/null
+++ b/app/assets/javascripts/components/checklist_item_form.js.jsx
@@ -0,0 +1,50 @@
+window.ChecklistItemForm = React.createClass({
+ formElement: function() { return React.findDOMNode(this.refs.form); },
+
+ componentDidMount: function() {
+ var self = this;
+
+ $(this.formElement()).on('ajax:success.ChecklistItem', function(e, newItem) {
+ self.props.formAjaxSuccess(newItem)
+ }).on('ajax:error.ChecklistItem', function(e, xhr) {
+ alert(xhr.responseText)
+ });
+ },
+
+ componentWillUnmount: function() {
+ $(this.formElement()).off('ajax:success.ChecklistItem').off('ajax:error.ChecklistItem')
+ },
+
+ getInitialState: function() {
+ return { formName: this.props.item.name }
+ },
+
+ componentWillReceiveProps: function(newProps) {
+ this.setState({ formName: newProps.item.name })
+ },
+
+ handleFormChange: function(e) {
+ this.setState({ formName: e.target.value });
+ },
+
+ componentDidUpdate: function() {
+ if (this.props.focus) {
+ React.findDOMNode(this.refs.focusField).focus();
+ }
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+})
diff --git a/app/assets/javascripts/components/checklist_items.js.jsx b/app/assets/javascripts/components/checklist_items.js.jsx
new file mode 100644
index 0000000..0c96ec5
--- /dev/null
+++ b/app/assets/javascripts/components/checklist_items.js.jsx
@@ -0,0 +1,40 @@
+window.ChecklistItems = React.createClass({
+ getInitialState: function() {
+ return { editModeIdx: -1, items: this.props.items };
+ },
+
+ updateEditModeIdx: function(newIdx) {
+ this.setState({ editModeIdx: newIdx })
+ },
+
+ addChecklistItem: function(newItem) {
+ var newItems = this.state.items.slice(0);
+ newItems.push(newItem);
+ this.setState({ items: newItems, newItemFormName: '' });
+ },
+
+ removeItem: function(itemId) {
+ this.setState({items: items.filter(function(i) { return i.id !== itemId }) });
+ },
+
+ render: function() {
+ var self = this;
+ return (
+
+
+ {
+ this.state.items.map(function(item, idx) {
+ return
;
+ })
+ }
+
+
+
+
+ );
+ }
+})
diff --git a/app/assets/javascripts/components/react_test.js.jsx b/app/assets/javascripts/components/react_test.js.jsx
new file mode 100644
index 0000000..8152d64
--- /dev/null
+++ b/app/assets/javascripts/components/react_test.js.jsx
@@ -0,0 +1,7 @@
+var HelloMessage = React.createClass({
+ render: function() {
+ return Hello {this.props.name}
;
+ }
+});
+
+window.HelloMessage = HelloMessage
diff --git a/app/assets/stylesheets/checklists.sass b/app/assets/stylesheets/checklists.sass
index 9a625eb..0516638 100644
--- a/app/assets/stylesheets/checklists.sass
+++ b/app/assets/stylesheets/checklists.sass
@@ -3,28 +3,17 @@
.btn
vertical-align: middle
-.edit_checklist_item
- [data-edit-control]
- display: none
+.checklist-item-name
+ &:after
+ content: attr(data-edit-prompt)
+ margin-left: 5px
+ color: grey
- &[data-edit-mode]
- [data-edit-control]
- display: block
- .name
+ @media (min-width: 768px)
+ &:after
display: none
-
- &:not([data-edit-mode])
- .checklist-item-name
- &:after
- content: attr(data-edit-prompt)
- margin-left: 5px
- color: grey
-
- @media (min-width: 768px)
- &:after
- display: none
- &:hover:after
- display: inline
+ &:hover:after
+ display: inline
[data-comfortable-text]
@media (max-width: 767px)
diff --git a/app/controllers/checklist_items_controller.rb b/app/controllers/checklist_items_controller.rb
index 99d6c60..05b3158 100644
--- a/app/controllers/checklist_items_controller.rb
+++ b/app/controllers/checklist_items_controller.rb
@@ -24,7 +24,7 @@ def destroy
def save_and_render_checklist
if checklist_item.save
- render partial: 'checklists/checklist_item', locals: {item: checklist_item, checklist: checklist}
+ render json: checklist_item
else
render text: checklist_item.errors.full_messages.join("\n"), status: :bad_request
end
diff --git a/app/models/checklist.rb b/app/models/checklist.rb
index 2c800d5..80ee761 100644
--- a/app/models/checklist.rb
+++ b/app/models/checklist.rb
@@ -25,6 +25,19 @@ def apply_to_pull_with_files?(files)
files.any? { |f| f.filename =~ re }
end
+ def as_json(options = {})
+ {
+ id: id,
+ name: name,
+ repository_path: UrlHelpers.github_repository_path(github_repository),
+ edit_path: UrlHelpers.edit_checklist_path(self),
+ items: checklist_items.sort_by(&:id).map(&:as_json),
+ github_repository_full_name: github_repository.github_full_name,
+ index_path: UrlHelpers.checklists_path,
+ create_item_path: UrlHelpers.checklist_checklist_items_path(id)
+ }
+ end
+
protected
def hook_repository
diff --git a/app/models/checklist_item.rb b/app/models/checklist_item.rb
index 3928833..c1da271 100644
--- a/app/models/checklist_item.rb
+++ b/app/models/checklist_item.rb
@@ -6,4 +6,12 @@ class ChecklistItem < ActiveRecord::Base
def to_markdown
"- [ ] #{name}"
end
+
+ def as_json(options = {})
+ {
+ id: id,
+ name: name,
+ path: UrlHelpers.polymorphic_path([checklist, self])
+ }
+ end
end
diff --git a/app/views/checklists/_checklist_item.html.haml b/app/views/checklists/_checklist_item.html.haml
deleted file mode 100644
index a2fd270..0000000
--- a/app/views/checklists/_checklist_item.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-.row
- = render 'checklists/checklist_item_form', checklist: checklist, item: item
- .col-xs-2.col-md-5
- = link_to '', checklist_checklist_item_path(checklist, item), class: 'destroy-checklist-item btn btn-danger btn-sm', method: :delete, remote: true, :'data-disable' => true, :'data-comfortable-text' => 'Remove', :'data-abbreviated-text' => 'X'
diff --git a/app/views/checklists/_checklist_item_form.html.haml b/app/views/checklists/_checklist_item_form.html.haml
deleted file mode 100644
index 05559ad..0000000
--- a/app/views/checklists/_checklist_item_form.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- placeholder ||= nil
-= form_for [checklist, item], remote: true do |f|
- .col-xs-6.col-md-4
- - unless item.new_record?
- .checklist-item-name{'data-edit-prompt' => 'Edit'}
- %span.name= item.name
- = f.text_field :name, placeholder: placeholder, class: 'form-control', :'data-edit-control' => true
- .col-xs-2.col-md-1
- = f.submit 'Save', :'data-disable-with' => 'Saving..', class: 'btn btn-primary btn-sm', :'data-edit-control' => true
diff --git a/app/views/checklists/_checklist_items.html.haml b/app/views/checklists/_checklist_items.html.haml
deleted file mode 100644
index 1d14114..0000000
--- a/app/views/checklists/_checklist_items.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-.checklist-items
- .row
- .col-xs-10
- %h4 Item
- - checklist.checklist_items.each do |item|
- = render 'checklist_item', item: item, checklist: checklist
- .row
- = render 'checklist_item_form', checklist: checklist, item: ChecklistItem.new, placeholder: 'New item'
diff --git a/app/views/checklists/show.html.haml b/app/views/checklists/show.html.haml
index 6801c90..ee14f2e 100644
--- a/app/views/checklists/show.html.haml
+++ b/app/views/checklists/show.html.haml
@@ -1,12 +1,3 @@
%p#notice= notice
-%h3= link_to checklist.github_repository.github_full_name, github_repository_path(checklist.github_repository)
-%h3= checklist.name
-
-.row
- .col-xs-10
- = link_to 'Edit', edit_checklist_path(checklist)
- \|
- = link_to 'Back', checklists_path
-
-= render 'checklist_items', checklist: checklist
+= react_component('Checklist', checklist.as_json, {prerender: true})
diff --git a/app/views/static/index.html.haml b/app/views/static/index.html.haml
index b440bc0..269b7c7 100644
--- a/app/views/static/index.html.haml
+++ b/app/views/static/index.html.haml
@@ -1,3 +1,5 @@
+= react_component('HelloMessage', {name: 'Andrew'}, {prerender: true})
+
%h1 Welcome to Preflight!
- unless user_signed_in?
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 06d02bf..f071834 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -39,4 +39,6 @@
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
+
+ config.react.variant = :development
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index f2f3b42..b20fb56 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -79,4 +79,6 @@
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
+
+ config.react.variant = :production
end
diff --git a/config/projections.json b/config/projections.json
index 1574227..8ec5393 100644
--- a/config/projections.json
+++ b/config/projections.json
@@ -2,5 +2,13 @@
"app/workers/*_worker.rb" : {
"command": "worker",
"template": "class %SWorker\nend"
+ },
+ "app/assets/javascripts/components/*.js.es6.jsx" : {
+ "command": "component",
+ "template": ""
+ },
+ "app/assets/javascripts/components/*.js.jsx" : {
+ "command": "component",
+ "template": ""
}
}
diff --git a/lib/url_helpers.rb b/lib/url_helpers.rb
new file mode 100644
index 0000000..a621402
--- /dev/null
+++ b/lib/url_helpers.rb
@@ -0,0 +1,5 @@
+module UrlHelpers
+ class << self
+ include Rails.application.routes.url_helpers
+ end
+end