Commit 0ff0d801 authored by Florian Hübsch's avatar Florian Hübsch
Browse files

Basic collection setup. Includes:

- Add ancestry gem for tree structure of collections.
- Add some collection seeds.
- Collection API via Grape gem.
- Collection Store/Actions
- Collection Tree View
- Add Navigation component
- Basic Bootstrap Layout
- UIStore/UIActions for Collection Selection
parent 2477a94a
...@@ -28,6 +28,14 @@ gem 'browserify-rails' ...@@ -28,6 +28,14 @@ gem 'browserify-rails'
# react specific gems # react specific gems
gem 'react-rails', '~> 1.1.0' gem 'react-rails', '~> 1.1.0'
# for collection tree structure
gem 'ancestry'
# API
gem 'grape'
gem 'hashie-forbidden_attributes'
gem 'grape-active_model_serializers'
group :development, :test do group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console # Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug' gem 'byebug'
......
...@@ -20,6 +20,8 @@ GEM ...@@ -20,6 +20,8 @@ GEM
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.1) rails-html-sanitizer (~> 1.0, >= 1.0.1)
active_model_serializers (0.9.3)
activemodel (>= 3.2)
activejob (4.2.0) activejob (4.2.0)
activesupport (= 4.2.0) activesupport (= 4.2.0)
globalid (>= 0.3.0) globalid (>= 0.3.0)
...@@ -36,10 +38,16 @@ GEM ...@@ -36,10 +38,16 @@ GEM
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
ancestry (2.1.0)
activerecord (>= 3.0.0)
arel (6.0.0) arel (6.0.0)
autoprefixer-rails (5.2.0.1) autoprefixer-rails (5.2.0.1)
execjs execjs
json json
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
babel-source (5.8.9) babel-source (5.8.9)
babel-transpiler (0.7.0) babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6) babel-source (>= 4.0, < 6)
...@@ -56,6 +64,8 @@ GEM ...@@ -56,6 +64,8 @@ GEM
builder (3.2.2) builder (3.2.2)
byebug (5.0.0) byebug (5.0.0)
columnize (= 0.9.0) columnize (= 0.9.0)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (4.1.0) coffee-rails (4.1.0)
coffee-script (>= 2.2.0) coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.0)
...@@ -67,6 +77,8 @@ GEM ...@@ -67,6 +77,8 @@ GEM
connection_pool (2.2.0) connection_pool (2.2.0)
daemons (1.2.2) daemons (1.2.2)
debug_inspector (0.0.2) debug_inspector (0.0.2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (3.5.1) devise (3.5.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
...@@ -75,6 +87,7 @@ GEM ...@@ -75,6 +87,7 @@ GEM
thread_safe (~> 0.1) thread_safe (~> 0.1)
warden (~> 1.2.3) warden (~> 1.2.3)
diff-lcs (1.2.5) diff-lcs (1.2.5)
equalizer (0.0.11)
erubis (2.7.0) erubis (2.7.0)
eventmachine (1.0.7) eventmachine (1.0.7)
execjs (2.5.2) execjs (2.5.2)
...@@ -85,6 +98,19 @@ GEM ...@@ -85,6 +98,19 @@ GEM
railties (>= 3.0.0) railties (>= 3.0.0)
globalid (0.3.5) globalid (0.3.5)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
grape (0.12.0)
activesupport
builder
hashie (>= 2.1.0)
multi_json (>= 1.3.2)
multi_xml (>= 0.5.2)
rack (>= 1.3.0)
rack-accept
rack-mount
virtus (>= 1.0.0)
grape-active_model_serializers (1.3.2)
active_model_serializers (>= 0.9.0)
grape
haml (4.0.6) haml (4.0.6)
tilt tilt
haml-rails (0.9.0) haml-rails (0.9.0)
...@@ -93,12 +119,16 @@ GEM ...@@ -93,12 +119,16 @@ GEM
haml (>= 4.0.6, < 5.0) haml (>= 4.0.6, < 5.0)
html2haml (>= 1.0.1) html2haml (>= 1.0.1)
railties (>= 4.0.1) railties (>= 4.0.1)
hashie (3.4.2)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
html2haml (2.0.0) html2haml (2.0.0)
erubis (~> 2.7.0) erubis (~> 2.7.0)
haml (~> 4.0.0) haml (~> 4.0.0)
nokogiri (~> 1.6.0) nokogiri (~> 1.6.0)
ruby_parser (~> 3.5) ruby_parser (~> 3.5)
i18n (0.7.0) i18n (0.7.0)
ice_nine (0.11.1)
jbuilder (2.3.0) jbuilder (2.3.0)
activesupport (>= 3.0.0, < 5) activesupport (>= 3.0.0, < 5)
multi_json (~> 1.2) multi_json (~> 1.2)
...@@ -115,11 +145,16 @@ GEM ...@@ -115,11 +145,16 @@ GEM
mini_portile (0.6.2) mini_portile (0.6.2)
minitest (5.7.0) minitest (5.7.0)
multi_json (1.11.1) multi_json (1.11.1)
multi_xml (0.5.5)
nokogiri (1.6.6.2) nokogiri (1.6.6.2)
mini_portile (~> 0.6.0) mini_portile (~> 0.6.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pg (0.18.2) pg (0.18.2)
rack (1.6.2) rack (1.6.2)
rack-accept (0.4.5)
rack (>= 0.4)
rack-mount (0.8.3)
rack (>= 1.0.0)
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.0) rails (4.2.0)
...@@ -209,6 +244,11 @@ GEM ...@@ -209,6 +244,11 @@ GEM
uglifier (2.7.1) uglifier (2.7.1)
execjs (>= 0.3.0) execjs (>= 0.3.0)
json (>= 1.8.0) json (>= 1.8.0)
virtus (1.0.5)
axiom-types (~> 0.1)
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.3) warden (1.2.3)
rack (>= 1.0) rack (>= 1.0)
web-console (2.1.2) web-console (2.1.2)
...@@ -221,12 +261,16 @@ PLATFORMS ...@@ -221,12 +261,16 @@ PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
ancestry
bootstrap-sass (~> 3.3.5) bootstrap-sass (~> 3.3.5)
browserify-rails browserify-rails
byebug byebug
devise devise
factory_girl_rails factory_girl_rails
grape
grape-active_model_serializers
haml-rails (~> 0.9) haml-rails (~> 0.9)
hashie-forbidden_attributes
jbuilder (~> 2.0) jbuilder (~> 2.0)
jquery-rails jquery-rails
pg pg
......
...@@ -17,3 +17,11 @@ ...@@ -17,3 +17,11 @@
# Available Seeds # Available Seeds
A user is seeded with email `test@ninjaconcept.com` and password `ninjaconcept`. A user is seeded with email `test@ninjaconcept.com` and password `ninjaconcept`.
# API (v1)
## Collections
* Get serialized collection roots
`/api/v1/collections/roots`
class API < Grape::API
prefix 'api'
version 'v1'
formatter :json, Grape::Formatter::ActiveModelSerializers
mount Chemotion::CollectionAPI
end
module Chemotion
class CollectionAPI < Grape::API
resource :collections do
desc "Return all serialized collection roots"
get :roots do
Collection.roots
end
end
end
end
...@@ -2,20 +2,9 @@ ...@@ -2,20 +2,9 @@
//= require react_ujs //= require react_ujs
React = require('react'); React = require('react');
Test = require('./components/test'); App = require('./components/App');
// fetch example
fetch('/').then(function(response) {
return response.text()
}).then(function(body) {
console.log(body)
});
// Immutable example // Immutable example
const Immutable = require('immutable'); const Immutable = require('immutable');
var map = Immutable.Map({foo: 'bar'}); var map = Immutable.Map({foo: 'bar'});
console.log(map.get('foo')); console.log(map.get('foo'));
// alt initialization
const Alt = require('alt');
const alt = new Alt();
import React from 'react';
import {Col, Grid, Row} from 'react-bootstrap';
import Navigation from './Navigation';
import CollectionTree from './CollectionTree';
class App extends React.Component {
constructor(props) {
super();
}
render() {
return (
<Grid fluid>
<Row>
<Navigation />
</Row>
<Row>
<Col md={3}>
<CollectionTree />
</Col>
<Col md={9}>
</Col>
</Row>
</Grid>
)
}
}
module.exports = App;
import React from 'react';
import {Button} from 'react-bootstrap';
import UIStore from './stores/UIStore';
import UIActions from './actions/UIActions';
class CollectionSubtree extends React.Component {
constructor(props) {
super();
this.state = {
label: props.root.label,
selected: false,
root: props.root,
visible: false
}
}
componentDidMount() {
UIStore.listen(this.onChange.bind(this));
}
componentWillUnmount() {
UIStore.unlisten(this.onChange.bind(this));
}
onChange(state) {
if(state.selectedCollectionIds[0] == this.state.root.id) {
// TODO trigger associated elements
// selectAssociatedSamples(collection_id)
this.setState({selected: true});
} else {
this.setState({selected: false});
}
}
selectedCssClass() {
return this.state.selected ? "selected" : "";
}
children() {
return this.state.root.children;
}
hasChildren() {
return this.children().length > 0;
}
subtrees() {
var children = this.children();
if(this.hasChildren()) {
return children.map((child, index) => {
return (
<li key={index}>
<CollectionSubtree root={child} />
</li>
)
})
} else {
return null;
}
}
expandButton() {
var label = this.state.visible ? '-' : '+';
if(this.hasChildren()) {
return (
<Button bsStyle="success" bsSize="xsmall" onClick={this.toggleExpansion.bind(this)}>
{label}
</Button>
)
}
}
handleClick() {
UIActions.deselectAllElements('collection');
UIActions.selectElement({type: 'collection', id: this.state.root.id});
}
toggleExpansion(e) {
e.stopPropagation();
this.setState({visible: !this.state.visible});
}
render() {
var style;
if (!this.state.visible) {
style = {display: "none"};
}
return (
<div className="tree-view">
<div className={"title " + this.selectedCssClass()} onClick={this.handleClick.bind(this)}>
{this.expandButton()}
{this.state.label}
</div>
<ul style={style}>
{this.subtrees()}
</ul>
</div>
)
}
}
module.exports = CollectionSubtree;
import React from 'react';
import {Button} from 'react-bootstrap';
import CollectionStore from './stores/CollectionStore';
import CollectionActions from './actions/CollectionActions';
import CollectionSubtree from './CollectionSubtree';
class CollectionTree extends React.Component {
constructor() {
super();
this.state = CollectionStore.getState();
}
componentDidMount() {
CollectionStore.listen(this.onChange.bind(this));
CollectionActions.fetchCollections();
}
componentWillUnmount() {
CollectionStore.unlisten(this.onChange.bind(this));
}
onChange(state) {
this.setState(state);
}
subtrees() {
var roots = this.state.collections;
if(roots.length > 0) {
return roots.map((root, index) => {
return <CollectionSubtree key={index} root={root} />
});
} else {
return <div></div>;
}
}
render() {
return (
<div className="tree-wrapper">
{this.subtrees()}
</div>
)
}
}
module.exports = CollectionTree;
import React from 'react';
import {Nav, Navbar, NavItem} from 'react-bootstrap';
class Navigation extends React.Component {
constructor(props) {
super();
}
render() {
return (
<Navbar brand="Chemotion" inverse>
<Nav>
<NavItem>#1</NavItem>
</Nav>
</Navbar>
)
}
}
module.exports = Navigation;
import alt from '../alt';
import CollectionsFetcher from '../fetchers/CollectionsFetcher';
class CollectionActions {
// TODO #1 unify naming: fetchCollections or fetchCollectionRoots?
// or do we need both?
// TODO #2...centralized error handling maybe ErrorActions?
fetchCollections() {
CollectionsFetcher.fetchRoots()
.then((roots) => {
this.dispatch(roots);
}).catch((errorMessage) => {
console.log(errorMessage);
});
}
}
export default alt.createActions(CollectionActions);
import alt from '../alt';
class UIActions {
selectElement(element) {
this.dispatch(element);
}
deselectAllElements(type) {
this.dispatch(type);
}
}
export default alt.createActions(UIActions);
// alt initialization
const Alt = require('alt');
const alt = new Alt();
module.exports = alt;
var CollectionsFetcher = {
fetchRoots() {
var promise = fetch('/api/v1/collections/roots.json')
.then((response) => {
return response.json()
}).then((json) => {
return json;
}).catch((errorMessage) => {
console.log(errorMessage);
});
return promise;
}
}
module.exports = CollectionsFetcher;
import alt from '../alt';
import CollectionActions from '../actions/CollectionActions';
class CollectionStore {
constructor() {
this.state = {
collections: []
};
this.bindListeners({
handleFetchCollections: CollectionActions.fetchCollections
})
}
handleFetchCollections(results) {
this.state.collections = results.collections;
}
}
export default alt.createStore(CollectionStore, 'CollectionStore');
import alt from '../alt';
import UIActions from '../actions/UIActions';
class UIStore {
constructor() {
this.state = {
selectedCollectionIds: []
};
this.bindListeners({
handleDeselectAllElements: UIActions.deselectAllElements,
handleSelectElement: UIActions.selectElement
})
}
handleDeselectAllElements(type) {
switch(type) {
case 'collection':
this.state.selectedCollectionIds = [];
break;
}
}
handleSelectElement(element) {
switch(element.type) {
case 'collection':
this.state.selectedCollectionIds.push(element.id);
break;
}
}
}
export default alt.createStore(UIStore, 'UIStore');
// Example component for collection stores,...
import React from 'react'; import React from 'react';
import {Button} from 'react-bootstrap'; import {Button} from 'react-bootstrap';
import CollectionStore from './stores/CollectionStore';
import CollectionActions from './actions/CollectionActions';