Ability to persistantly add crops to garden 💾
parent
99b68f74f4
commit
c8100098ba
1
Gemfile
1
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
|
||||
|
||||
|
|
10
Gemfile.lock
10
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
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
#
|
||||
class Crop
|
||||
include Mongoid::Document
|
||||
|
||||
belongs_to :device
|
||||
|
||||
field :x, type: Integer
|
||||
field :y, type: Integer
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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" %>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
var initialState = {
|
||||
leftMenu: {
|
||||
component: 'CropInventory',
|
||||
tab: 'Plants'
|
||||
},
|
||||
middleMenu: {
|
||||
mapPoints: []
|
||||
},
|
||||
rightMenu: {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
|
|
15
package.json
15
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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue