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 '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
|
||||||
|
|
||||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -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
|
||||||
|
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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 :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
|
||||||
|
|
|
@ -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 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" %>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 { 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);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
15
package.json
15
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue