Ability to persistantly add crops to garden 💾

pull/225/head
Rick Carlino 2015-09-29 15:10:24 -05:00
parent 99b68f74f4
commit c8100098ba
21 changed files with 155 additions and 76 deletions

View File

@ -38,7 +38,6 @@ group :development, :test do
gem 'pry' gem 'pry'
gem 'factory_girl_rails' gem 'factory_girl_rails'
gem 'faker' gem 'faker'
gem 'jasmine-rails'
gem 'smarf_doc', github: 'RickCarlino/smarf_doc' gem 'smarf_doc', github: 'RickCarlino/smarf_doc'
end end

View File

@ -153,12 +153,6 @@ GEM
i18n (0.7.0) i18n (0.7.0)
ice_cube (0.12.1) ice_cube (0.12.1)
ice_nine (0.11.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 (1.8.2)
json_pure (1.8.2) json_pure (1.8.2)
launchy (2.4.3) launchy (2.4.3)
@ -379,7 +373,6 @@ DEPENDENCIES
haml haml
high_voltage (~> 2.1.0) high_voltage (~> 2.1.0)
ice_cube ice_cube
jasmine-rails
launchy launchy
metric_fu metric_fu
mongoid (~> 4.0.0)! mongoid (~> 4.0.0)!
@ -405,3 +398,6 @@ DEPENDENCIES
simplecov simplecov
smarf_doc! smarf_doc!
uglifier uglifier
BUNDLED WITH
1.10.6

View File

@ -2,3 +2,13 @@
//= require jquery //= require jquery
//= require react //= require react
//= require react_ujs //= 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 );
}
});
});

View File

@ -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

View File

@ -0,0 +1,9 @@
#
class Crop
include Mongoid::Document
belongs_to :device
field :x, type: Integer
field :y, type: Integer
end

View File

@ -6,6 +6,7 @@ class Device
has_many :users has_many :users
has_many :schedules, dependent: :destroy has_many :schedules, dependent: :destroy
has_many :sequences has_many :sequences
has_many :crops, dependent: :destroy
# The SkyNet UUID of the device # The SkyNet UUID of the device

View File

@ -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

View File

@ -1,4 +1,18 @@
<div class="farm-designer" id="root"> <div class="farm-designer" id="root">
</div> </div>
<script type="text/javascript">
window.initialState = {
leftMenu: {
component: 'CropInventory',
tab: 'Plants'
},
middleMenu: {
crops: <%= raw(current_user.device.crops.to_json) %>
},
rightMenu: {
}
};
</script>
<%= javascript_include_tag "/build/farm-designer.js" %> <%= javascript_include_tag "/build/farm-designer.js" %>

View File

@ -1,8 +1,8 @@
FarmBot::Application.routes.draw do FarmBot::Application.routes.draw do
mount JasmineRails::Engine => '/specs' if defined?(JasmineRails)
namespace :api, defaults: {format: :json} do namespace :api, defaults: {format: :json} do
resource :device, only: [:show, :destroy, :create, :update] resource :device, only: [:show, :destroy, :create, :update]
resources :crops, only: [:create, :destroy, :index]
resources :sequences, only: [:create, :update, :destroy, :index, :show] do resources :sequences, only: [:create, :update, :destroy, :index, :show] do
resources :steps, only: [:show, :create, :index, :update, :destroy] resources :steps, only: [:show, :create, :index, :update, :destroy]
end end

View File

@ -1,9 +1,11 @@
export class Crop { export class Crop {
constructor(options) { constructor(options) {
this.name = (options.name || "Untitled Crop"); this.name = (options.name || "Untitled Crop");
this.age = (options.age || _.random(0, 5)); this.age = (options.age || _.random(0, 5));
this._id = (options._id || _.random(0, 1000)); this._id = (options._id || _.random(0, 1000));
this.imgUrl = (options.imgUrl || "/designer_icons/unknown.svg"); this.imgUrl = (options.imgUrl || "/designer_icons/unknown.svg");
this.x = (options.x || 0);
this.y = (options.y || 0);
} }
}; };

View File

@ -1,4 +1,4 @@
import React from 'react'; import React from 'react/addons';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from './redux/store'; import { store } from './redux/store';
import { connect } from 'react-redux'; import { connect } from 'react-redux';

View File

@ -1,25 +1,9 @@
class MapPoint { import { Crop } from '../crops';
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 <img style={style} src="/designer_icons/pin.png"></img>;
}
};
export class CropInfo extends React.Component { export class CropInfo extends React.Component {
drop (e) { drop (e) {
var data = this.state.data.concat(new MapPoint(e.clientX, e.clientY)); var crop = new Crop({x: e.clientX, y: e.clientY});
this.setState({data: data}); this.props.dispatch({type: "CROP_ADD_REQUEST", payload: crop})
} }
constructor() { constructor() {
@ -28,13 +12,6 @@ export class CropInfo extends React.Component {
this.state = {data: []}; this.state = {data: []};
} }
get points() {
var points = this.state.data.map(
(p, k) => <MapPointView point={ p } key={k} />
);
return points;
}
showCatalog(){ showCatalog(){
this.props.dispatch({type: "CATALOG_SHOW"}) this.props.dispatch({type: "CATALOG_SHOW"})
} }
@ -105,9 +82,6 @@ export class CropInfo extends React.Component {
Delete Delete
</button> </button>
</span> </span>
<div id="drop-area">
{ this.points }
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,15 +6,15 @@ import { renderCatalog } from './plant_catalog';
export class Tab extends React.Component { export class Tab extends React.Component {
render() { render() {
return <li onClick={ this.handleClick.bind(this) }> return <li onClick={ this.handleClick.bind(this) }>
<a href="#" <a href="#"
className={this.props.active ? "active" : ""}> className={this.props.active ? "active" : ""}>
{ this.props.name } { this.props.name }
</a> </a>
</li> </li>
} }
handleClick() { handleClick() {
this.props.dispatch({type: "INVENTORY_SHOW_TAB", tab: this.props.name}); this.props.dispatch({type: "INVENTORY_SHOW_TAB", payload: this.props.name});
} }
} }

View File

@ -10,6 +10,7 @@ export class DesignerMain extends React.Component {
transferableProps(name){ transferableProps(name){
return _.merge({}, {dispatch: this.props.dispatch}, this.props[name]); return _.merge({}, {dispatch: this.props.dispatch}, this.props[name]);
}; };
// Dynamically determine what to render on the left side of the designer, // Dynamically determine what to render on the left side of the designer,
// based on the value of getStore().leftMenu.component // based on the value of getStore().leftMenu.component
renderLeft() { renderLeft() {
@ -17,10 +18,12 @@ export class DesignerMain extends React.Component {
let component = LEFT_MENU_CHOICES[props.component]; let component = LEFT_MENU_CHOICES[props.component];
return React.createElement(component, props); return React.createElement(component, props);
} }
renderMiddle(){ renderMiddle(){
let props = this.transferableProps("middleMenu"); let props = this.transferableProps("middleMenu");
return React.createElement(GardenMap, props); return React.createElement(GardenMap, props);
} }
render(){ render(){
return ( return (
<div className="farm-designer-body"> <div className="farm-designer-body">

View File

@ -1,5 +1,24 @@
export class GardenMap extends React.Component { export class MapPointView extends React.Component {
render() { render() {
return <div>Hello, GardenMap</div>; var style = {
position: 'absolute',
left: (this.props.point.x - 20),
top: (this.props.point.y - 40)
};
return <img style={style} src="/designer_icons/pin.png"></img>;
}
};
export class GardenMap extends React.Component {
points() {
return this.props.crops.map((p, k) => <MapPointView point={ p } key={k} />);
}
render() {
return <div>
<div id="drop-area">
{ this.points() }
</div>
</div>;
} }
} }

View File

@ -4,7 +4,7 @@ export class PlantCatalogTile extends React.Component {
showCropInfo(){ showCropInfo(){
this.props.dispatch({ this.props.dispatch({
type: 'CROP_INFO_SHOW', type: 'CROP_INFO_SHOW',
crop: this.props.crop payload: this.props.crop
}); });
}; };

View File

@ -1,3 +1,7 @@
//actually, these are 'action creators'.
import { store } from './store';
import { addons } from 'react/addons';
let actions = { let actions = {
'@@redux/INIT': empty, '@@redux/INIT': empty,
DEFAULT: function (s, a) { DEFAULT: function (s, a) {
@ -5,12 +9,28 @@ let actions = {
console.trace(); console.trace();
return s; 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) { CROP_INFO_SHOW: function(s, a) {
// TODO: add type system to check for presence of `crop` Object? // TODO: add type system to check for presence of `crop` Object?
let fragment = { let fragment = {
leftMenu: { leftMenu: {
component: 'CropInfo', component: 'CropInfo',
crop: a.crop crop: a.payload
} }
}; };
return update(s, fragment); return update(s, fragment);
@ -22,8 +42,8 @@ let actions = {
return changeLeftComponent(s, 'CropInventory'); return changeLeftComponent(s, 'CropInventory');
}, },
INVENTORY_SHOW_TAB: function(s, a) { INVENTORY_SHOW_TAB: function(s, a) {
return update(s, {leftMenu: {tab: a.tab}}); return update(s, {leftMenu: {tab: a.payload}});
}, }
} }
function empty(s, a) { function empty(s, a) {

View File

@ -1,14 +0,0 @@
var initialState = {
leftMenu: {
component: 'CropInventory',
tab: 'Plants'
},
middleMenu: {
mapPoints: []
},
rightMenu: {
}
};
export { initialState };

View File

@ -1,6 +1,10 @@
import { actions } from './actions'; import { actions } from './actions';
import { isFSA } from 'flux-standard-action';
export function reducer(state, action) { export function reducer(state, action) {
console.log(action.type) if (isFSA(action)){
return (actions[action.type] || actions.DEFAULT)(state, action); return (actions[action.type] || actions.DEFAULT)(state, action);
} else {
console.error("Action does not conform to 'flux-standard-action", action);
};
}; };

View File

@ -1,7 +1,7 @@
import { createStore } from 'redux'; import { createStore } from 'redux';
import { initialState } from './initial_state';
import { reducer } from './reducer'; import { reducer } from './reducer';
var store = createStore(reducer, initialState); // var store = createStore(reducer, initialState);
var store = createStore(reducer, window.initialState);
export { store }; export { store };

View File

@ -17,22 +17,25 @@
}, },
"homepage": "https://github.com/rickcarlino/farmbot-web-app", "homepage": "https://github.com/rickcarlino/farmbot-web-app",
"browserify": { "browserify": {
"transform": ["babelify"] "transform": [
"babelify"
]
}, },
"dependencies": { "dependencies": {
"angular": "^1.3.19", "angular": "^1.3.19",
"angular-ui-sortable": "^0.13.4", "angular-ui-sortable": "^0.13.4",
"babelify": "^6.3.0",
"browserify": "^11.1.0", "browserify": "^11.1.0",
"browserify-incremental": "^3.0.1", "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": "^0.13.3",
"react-redux": "^2.1.2", "react-redux": "^2.1.2",
"reactify": "^1.1.1", "reactify": "^1.1.1",
"redux": "^3.0.0", "redux": "^3.0.0",
"gulp": "^3.9.0", "redux-router": "^1.0.0-beta3",
"babelify": "^6.3.0",
"browserify": "^11.1.0",
"gulp-concat": "^2.6.0",
"gulp-util": "^3.0.6",
"vinyl-source-stream": "^1.1.0" "vinyl-source-stream": "^1.1.0"
} }
} }