From c8100098ba7cc4a6d0be42b419408d8593794f95 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Tue, 29 Sep 2015 15:10:24 -0500 Subject: [PATCH] Ability to persistantly add crops to garden :floppy_disk: --- Gemfile | 1 - Gemfile.lock | 10 +++---- app/assets/javascripts/application.js.jsx | 10 +++++++ app/controllers/api/crops_controller.rb | 26 ++++++++++++++++++ app/models/crop.rb | 9 +++++++ app/models/device.rb | 1 + app/mutations/crops/create.rb | 13 +++++++++ app/views/pages/farm_designer.html.erb | 14 ++++++++++ config/routes.rb | 2 +- javascripts/crops.js | 6 +++-- javascripts/farm_designer.js | 2 +- javascripts/menus/crop_info.js | 32 +++-------------------- javascripts/menus/crop_inventory.js | 10 +++---- javascripts/menus/designer_main.js | 3 +++ javascripts/menus/garden_map.js | 23 ++++++++++++++-- javascripts/menus/plant_catalog.js | 2 +- javascripts/redux/actions.js | 26 +++++++++++++++--- javascripts/redux/initial_state.js | 14 ---------- javascripts/redux/reducer.js | 8 ++++-- javascripts/redux/store.js | 4 +-- package.json | 15 ++++++----- 21 files changed, 155 insertions(+), 76 deletions(-) create mode 100644 app/controllers/api/crops_controller.rb create mode 100644 app/models/crop.rb create mode 100644 app/mutations/crops/create.rb delete mode 100644 javascripts/redux/initial_state.js diff --git a/Gemfile b/Gemfile index 3f7b5d986..00fbaa298 100755 --- a/Gemfile +++ b/Gemfile @@ -38,7 +38,6 @@ group :development, :test do gem 'pry' gem 'factory_girl_rails' gem 'faker' - gem 'jasmine-rails' gem 'smarf_doc', github: 'RickCarlino/smarf_doc' end diff --git a/Gemfile.lock b/Gemfile.lock index 3bcc56918..9e5a9e835 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,12 +153,6 @@ GEM i18n (0.7.0) ice_cube (0.12.1) ice_nine (0.11.1) - jasmine-core (2.2.0) - jasmine-rails (0.10.7) - jasmine-core (>= 1.3, < 3.0) - phantomjs (< 2.0) - railties (>= 3.1.0) - sprockets-rails json (1.8.2) json_pure (1.8.2) launchy (2.4.3) @@ -379,7 +373,6 @@ DEPENDENCIES haml high_voltage (~> 2.1.0) ice_cube - jasmine-rails launchy metric_fu mongoid (~> 4.0.0)! @@ -405,3 +398,6 @@ DEPENDENCIES simplecov smarf_doc! uglifier + +BUNDLED WITH + 1.10.6 diff --git a/app/assets/javascripts/application.js.jsx b/app/assets/javascripts/application.js.jsx index f6382f620..733fc6a62 100644 --- a/app/assets/javascripts/application.js.jsx +++ b/app/assets/javascripts/application.js.jsx @@ -2,3 +2,13 @@ //= require jquery //= require react //= require react_ujs +$(function(){ + // Append Rails CSRF token to requests. + var token = $( 'meta[name="csrf-token"]' ).attr( 'content' ); + + $.ajaxSetup( { + beforeSend: function ( xhr ) { + xhr.setRequestHeader( 'X-CSRF-Token', token ); + } + }); +}); diff --git a/app/controllers/api/crops_controller.rb b/app/controllers/api/crops_controller.rb new file mode 100644 index 000000000..e8bf4953e --- /dev/null +++ b/app/controllers/api/crops_controller.rb @@ -0,0 +1,26 @@ +module Api + class CropsController < Api::AbstractController + + def index + render json: Crop.where(device: current_device) + end + + def create + mutate Crops::Create.run(params, device: current_device) + end + + def destroy + if (crop.device == current_device) && crop.destroy + render nothing: true + else + raise Errors::Forbidden, "Not your Crop object." + end + end + + private + + def crop + @crop ||= Crop.find(params[:id]) + end + end +end diff --git a/app/models/crop.rb b/app/models/crop.rb new file mode 100644 index 000000000..4dae153e0 --- /dev/null +++ b/app/models/crop.rb @@ -0,0 +1,9 @@ +# +class Crop + include Mongoid::Document + + belongs_to :device + + field :x, type: Integer + field :y, type: Integer +end diff --git a/app/models/device.rb b/app/models/device.rb index 464634da0..44d08852e 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -6,6 +6,7 @@ class Device has_many :users has_many :schedules, dependent: :destroy has_many :sequences + has_many :crops, dependent: :destroy # The SkyNet UUID of the device diff --git a/app/mutations/crops/create.rb b/app/mutations/crops/create.rb new file mode 100644 index 000000000..f25e14380 --- /dev/null +++ b/app/mutations/crops/create.rb @@ -0,0 +1,13 @@ +module Crops + class Create < Mutations::Command + required do + model :device, class: Device + integer :x + integer :y + end + + def execute + Crop.create!(inputs) + end + end +end diff --git a/app/views/pages/farm_designer.html.erb b/app/views/pages/farm_designer.html.erb index c60aed28d..028b3b6c5 100644 --- a/app/views/pages/farm_designer.html.erb +++ b/app/views/pages/farm_designer.html.erb @@ -1,4 +1,18 @@
+ + <%= javascript_include_tag "/build/farm-designer.js" %> diff --git a/config/routes.rb b/config/routes.rb index 982014b4d..820ab3cbc 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,8 +1,8 @@ FarmBot::Application.routes.draw do - mount JasmineRails::Engine => '/specs' if defined?(JasmineRails) namespace :api, defaults: {format: :json} do resource :device, only: [:show, :destroy, :create, :update] + resources :crops, only: [:create, :destroy, :index] resources :sequences, only: [:create, :update, :destroy, :index, :show] do resources :steps, only: [:show, :create, :index, :update, :destroy] end diff --git a/javascripts/crops.js b/javascripts/crops.js index f5c19e4dd..cc58c8a8f 100644 --- a/javascripts/crops.js +++ b/javascripts/crops.js @@ -1,9 +1,11 @@ export class Crop { constructor(options) { this.name = (options.name || "Untitled Crop"); - this.age = (options.age || _.random(0, 5)); - this._id = (options._id || _.random(0, 1000)); + this.age = (options.age || _.random(0, 5)); + this._id = (options._id || _.random(0, 1000)); this.imgUrl = (options.imgUrl || "/designer_icons/unknown.svg"); + this.x = (options.x || 0); + this.y = (options.y || 0); } }; diff --git a/javascripts/farm_designer.js b/javascripts/farm_designer.js index 9576bab25..4371d8abd 100644 --- a/javascripts/farm_designer.js +++ b/javascripts/farm_designer.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React from 'react/addons'; import { Provider } from 'react-redux'; import { store } from './redux/store'; import { connect } from 'react-redux'; diff --git a/javascripts/menus/crop_info.js b/javascripts/menus/crop_info.js index dbc14476e..c6082f3b5 100644 --- a/javascripts/menus/crop_info.js +++ b/javascripts/menus/crop_info.js @@ -1,25 +1,9 @@ -class MapPoint { - constructor(x, y) { - this.x = x || 0; - this.y = y || 0; - } -} - -export class MapPointView extends React.Component { - render() { - var style = { - position: 'absolute', - left: (this.props.point.x - 20), - top: (this.props.point.y - 40) - }; - return ; - } -}; +import { Crop } from '../crops'; export class CropInfo extends React.Component { drop (e) { - var data = this.state.data.concat(new MapPoint(e.clientX, e.clientY)); - this.setState({data: data}); + var crop = new Crop({x: e.clientX, y: e.clientY}); + this.props.dispatch({type: "CROP_ADD_REQUEST", payload: crop}) } constructor() { @@ -28,13 +12,6 @@ export class CropInfo extends React.Component { this.state = {data: []}; } - get points() { - var points = this.state.data.map( - (p, k) => - ); - return points; - } - showCatalog(){ this.props.dispatch({type: "CATALOG_SHOW"}) } @@ -105,9 +82,6 @@ export class CropInfo extends React.Component { Delete -
- { this.points } -
diff --git a/javascripts/menus/crop_inventory.js b/javascripts/menus/crop_inventory.js index 5814215ab..0019fbe7b 100644 --- a/javascripts/menus/crop_inventory.js +++ b/javascripts/menus/crop_inventory.js @@ -6,15 +6,15 @@ import { renderCatalog } from './plant_catalog'; export class Tab extends React.Component { render() { return
  • - - { this.props.name } - + + { this.props.name } +
  • } handleClick() { - this.props.dispatch({type: "INVENTORY_SHOW_TAB", tab: this.props.name}); + this.props.dispatch({type: "INVENTORY_SHOW_TAB", payload: this.props.name}); } } diff --git a/javascripts/menus/designer_main.js b/javascripts/menus/designer_main.js index 12aba630c..3d7226e2d 100644 --- a/javascripts/menus/designer_main.js +++ b/javascripts/menus/designer_main.js @@ -10,6 +10,7 @@ export class DesignerMain extends React.Component { transferableProps(name){ return _.merge({}, {dispatch: this.props.dispatch}, this.props[name]); }; + // Dynamically determine what to render on the left side of the designer, // based on the value of getStore().leftMenu.component renderLeft() { @@ -17,10 +18,12 @@ export class DesignerMain extends React.Component { let component = LEFT_MENU_CHOICES[props.component]; return React.createElement(component, props); } + renderMiddle(){ let props = this.transferableProps("middleMenu"); return React.createElement(GardenMap, props); } + render(){ return (
    diff --git a/javascripts/menus/garden_map.js b/javascripts/menus/garden_map.js index fc98aef4a..b5f68e436 100644 --- a/javascripts/menus/garden_map.js +++ b/javascripts/menus/garden_map.js @@ -1,5 +1,24 @@ -export class GardenMap extends React.Component { +export class MapPointView extends React.Component { render() { - return
    Hello, GardenMap
    ; + var style = { + position: 'absolute', + left: (this.props.point.x - 20), + top: (this.props.point.y - 40) + }; + return ; + } +}; + +export class GardenMap extends React.Component { + points() { + return this.props.crops.map((p, k) => ); + } + + render() { + return
    +
    + { this.points() } +
    +
    ; } } diff --git a/javascripts/menus/plant_catalog.js b/javascripts/menus/plant_catalog.js index 975a97d9f..800db46f4 100644 --- a/javascripts/menus/plant_catalog.js +++ b/javascripts/menus/plant_catalog.js @@ -4,7 +4,7 @@ export class PlantCatalogTile extends React.Component { showCropInfo(){ this.props.dispatch({ type: 'CROP_INFO_SHOW', - crop: this.props.crop + payload: this.props.crop }); }; diff --git a/javascripts/redux/actions.js b/javascripts/redux/actions.js index 98b2a2d56..50531fb45 100644 --- a/javascripts/redux/actions.js +++ b/javascripts/redux/actions.js @@ -1,3 +1,7 @@ +//actually, these are 'action creators'. +import { store } from './store'; +import { addons } from 'react/addons'; + let actions = { '@@redux/INIT': empty, DEFAULT: function (s, a) { @@ -5,12 +9,28 @@ let actions = { console.trace(); return s; }, + CROP_ADD_REQUEST: function (s, a) { + var req = $.ajax({method: "POST", url: "/api/crops", data: a.payload}) + .done(function (crop) { + store.dispatch({type: "CROP_ADD_SUCCESS", payload: crop}); + }) + .fail(function (a, b, c) { store.dispatch({type: "CROP_ADD_FAILURE"}) }); + return s; + }, + CROP_ADD_FAILURE: function (s = store.getState(), a) { + alert("Failed to add crop, and also failed to write an error handler :("); + return s; + }, + CROP_ADD_SUCCESS: function (s = store.getState(), a) { + var new_array = s.middleMenu.crops.concat(a.payload); + return update(s, {middleMenu: {crops: new_array}}); + }, CROP_INFO_SHOW: function(s, a) { // TODO: add type system to check for presence of `crop` Object? let fragment = { leftMenu: { component: 'CropInfo', - crop: a.crop + crop: a.payload } }; return update(s, fragment); @@ -22,8 +42,8 @@ let actions = { return changeLeftComponent(s, 'CropInventory'); }, INVENTORY_SHOW_TAB: function(s, a) { - return update(s, {leftMenu: {tab: a.tab}}); - }, + return update(s, {leftMenu: {tab: a.payload}}); + } } function empty(s, a) { diff --git a/javascripts/redux/initial_state.js b/javascripts/redux/initial_state.js deleted file mode 100644 index 7b9bd42d5..000000000 --- a/javascripts/redux/initial_state.js +++ /dev/null @@ -1,14 +0,0 @@ -var initialState = { - leftMenu: { - component: 'CropInventory', - tab: 'Plants' - }, - middleMenu: { - mapPoints: [] - }, - rightMenu: { - - } -}; - -export { initialState }; diff --git a/javascripts/redux/reducer.js b/javascripts/redux/reducer.js index 0a1d37040..280eb150b 100644 --- a/javascripts/redux/reducer.js +++ b/javascripts/redux/reducer.js @@ -1,6 +1,10 @@ import { actions } from './actions'; +import { isFSA } from 'flux-standard-action'; export function reducer(state, action) { - console.log(action.type) - return (actions[action.type] || actions.DEFAULT)(state, action); + if (isFSA(action)){ + return (actions[action.type] || actions.DEFAULT)(state, action); + } else { + console.error("Action does not conform to 'flux-standard-action", action); + }; }; diff --git a/javascripts/redux/store.js b/javascripts/redux/store.js index ca2e51a3e..1b6618019 100644 --- a/javascripts/redux/store.js +++ b/javascripts/redux/store.js @@ -1,7 +1,7 @@ import { createStore } from 'redux'; -import { initialState } from './initial_state'; import { reducer } from './reducer'; -var store = createStore(reducer, initialState); +// var store = createStore(reducer, initialState); +var store = createStore(reducer, window.initialState); export { store }; diff --git a/package.json b/package.json index 7ab7d981e..d05c81f8c 100644 --- a/package.json +++ b/package.json @@ -17,22 +17,25 @@ }, "homepage": "https://github.com/rickcarlino/farmbot-web-app", "browserify": { - "transform": ["babelify"] + "transform": [ + "babelify" + ] }, "dependencies": { "angular": "^1.3.19", "angular-ui-sortable": "^0.13.4", + "babelify": "^6.3.0", "browserify": "^11.1.0", "browserify-incremental": "^3.0.1", + "flux-standard-action": "^0.6.0", + "gulp": "^3.9.0", + "gulp-concat": "^2.6.0", + "gulp-util": "^3.0.6", "react": "^0.13.3", "react-redux": "^2.1.2", "reactify": "^1.1.1", "redux": "^3.0.0", - "gulp": "^3.9.0", - "babelify": "^6.3.0", - "browserify": "^11.1.0", - "gulp-concat": "^2.6.0", - "gulp-util": "^3.0.6", + "redux-router": "^1.0.0-beta3", "vinyl-source-stream": "^1.1.0" } }