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 'factory_girl_rails'
gem 'faker'
gem 'jasmine-rails'
gem 'smarf_doc', github: 'RickCarlino/smarf_doc'
end

View File

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

View File

@ -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 );
}
});
});

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 :schedules, dependent: :destroy
has_many :sequences
has_many :crops, dependent: :destroy
# 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>
<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" %>

View File

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

View File

@ -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);
}
};

View File

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

View File

@ -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 <img style={style} src="/designer_icons/pin.png"></img>;
}
};
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) => <MapPointView point={ p } key={k} />
);
return points;
}
showCatalog(){
this.props.dispatch({type: "CATALOG_SHOW"})
}
@ -105,9 +82,6 @@ export class CropInfo extends React.Component {
Delete
</button>
</span>
<div id="drop-area">
{ this.points }
</div>
</div>
</div>
</div>

View File

@ -6,15 +6,15 @@ import { renderCatalog } from './plant_catalog';
export class Tab extends React.Component {
render() {
return <li onClick={ this.handleClick.bind(this) }>
<a href="#"
className={this.props.active ? "active" : ""}>
{ this.props.name }
</a>
<a href="#"
className={this.props.active ? "active" : ""}>
{ this.props.name }
</a>
</li>
}
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){
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 (
<div className="farm-designer-body">

View File

@ -1,5 +1,24 @@
export class GardenMap extends React.Component {
export class MapPointView extends React.Component {
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(){
this.props.dispatch({
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 = {
'@@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) {

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 { 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);
};
};

View File

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

View File

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