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.github_repository_full_name}

+

{this.props.name}

+
+
+ Edit | Back +
+
+ +
+ ); + } +}) + +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 ( +
+
+
+

Item

+
+
+ { + 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