Adding React.js, Redux, revamping dashboard
parent
68c93f8b85
commit
49520d6e62
|
@ -20,3 +20,4 @@ public/system
|
||||||
public/assets
|
public/assets
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
node_modules/
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -38,6 +38,9 @@ gem 'rack-attack'
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
gem 'sinatra', require: nil, github: 'sinatra'
|
gem 'sinatra', require: nil, github: 'sinatra'
|
||||||
|
|
||||||
|
gem 'react-rails'
|
||||||
|
gem 'browserify-rails'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'rspec-rails'
|
gem 'rspec-rails'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
|
|
16
Gemfile.lock
16
Gemfile.lock
|
@ -53,6 +53,10 @@ GEM
|
||||||
addressable (2.4.0)
|
addressable (2.4.0)
|
||||||
arel (7.1.1)
|
arel (7.1.1)
|
||||||
ast (2.3.0)
|
ast (2.3.0)
|
||||||
|
babel-source (5.8.35)
|
||||||
|
babel-transpiler (0.7.0)
|
||||||
|
babel-source (>= 4.0, < 6)
|
||||||
|
execjs (~> 2.0)
|
||||||
bcrypt (3.1.11)
|
bcrypt (3.1.11)
|
||||||
better_errors (2.1.1)
|
better_errors (2.1.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
|
@ -60,6 +64,9 @@ GEM
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
|
browserify-rails (3.1.0)
|
||||||
|
railties (>= 4.0.0, < 5.1)
|
||||||
|
sprockets (>= 3.5.2)
|
||||||
builder (3.2.2)
|
builder (3.2.2)
|
||||||
bullet (5.3.0)
|
bullet (5.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -245,6 +252,13 @@ GEM
|
||||||
rake (11.2.2)
|
rake (11.2.2)
|
||||||
rdoc (4.2.2)
|
rdoc (4.2.2)
|
||||||
json (~> 1.4)
|
json (~> 1.4)
|
||||||
|
react-rails (1.8.2)
|
||||||
|
babel-transpiler (>= 0.7.0)
|
||||||
|
coffee-script-source (~> 1.8)
|
||||||
|
connection_pool
|
||||||
|
execjs
|
||||||
|
railties (>= 3.2)
|
||||||
|
tilt
|
||||||
redis (3.3.1)
|
redis (3.3.1)
|
||||||
ref (2.0.0)
|
ref (2.0.0)
|
||||||
responders (2.3.0)
|
responders (2.3.0)
|
||||||
|
@ -348,6 +362,7 @@ DEPENDENCIES
|
||||||
addressable
|
addressable
|
||||||
better_errors
|
better_errors
|
||||||
binding_of_caller
|
binding_of_caller
|
||||||
|
browserify-rails
|
||||||
bullet
|
bullet
|
||||||
coffee-rails (~> 4.1.0)
|
coffee-rails (~> 4.1.0)
|
||||||
devise
|
devise
|
||||||
|
@ -380,6 +395,7 @@ DEPENDENCIES
|
||||||
rails (= 5.0.0.1)
|
rails (= 5.0.0.1)
|
||||||
rails_12factor
|
rails_12factor
|
||||||
rails_autolink
|
rails_autolink
|
||||||
|
react-rails
|
||||||
redis (~> 3.2)
|
redis (~> 3.2)
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rspec-sidekiq
|
rspec-sidekiq
|
||||||
|
|
|
@ -12,4 +12,6 @@
|
||||||
//
|
//
|
||||||
//= require jquery
|
//= require jquery
|
||||||
//= require jquery_ujs
|
//= require jquery_ujs
|
||||||
//= require_tree .
|
//= require components
|
||||||
|
//= require cable
|
||||||
|
//= require mastodon-logo
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
App.timeline = App.cable.subscriptions.create("TimelineChannel", {
|
|
||||||
connected: function() {
|
|
||||||
console.log('Connected');
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnected: function() {
|
|
||||||
console.log('Disconnected');
|
|
||||||
},
|
|
||||||
|
|
||||||
received: function(data) {
|
|
||||||
console.log(JSON.parse(data.message));
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
//= require_self
|
||||||
|
//= require react_ujs
|
||||||
|
|
||||||
|
window.React = require('react');
|
||||||
|
window.ReactDOM = require('react-dom');
|
||||||
|
|
||||||
|
//= require_tree ./components
|
||||||
|
|
||||||
|
window.Root = require('./components/containers/root');
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const SET_TIMELINE = 'SET_TIMELINE';
|
||||||
|
export const ADD_STATUS = 'ADD_STATUS';
|
||||||
|
|
||||||
|
export function setTimeline(timeline, statuses) {
|
||||||
|
return {
|
||||||
|
type: SET_TIMELINE,
|
||||||
|
timeline: timeline,
|
||||||
|
statuses: statuses
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addStatus(timeline, status) {
|
||||||
|
return {
|
||||||
|
type: ADD_STATUS,
|
||||||
|
timeline: timeline,
|
||||||
|
status: status
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import StatusListContainer from '../containers/status_list_container';
|
||||||
|
import ColumnHeader from './column_header';
|
||||||
|
|
||||||
|
const Column = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
type: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '350px', flex: '0 0 auto', background: '#282c37', margin: '10px', marginRight: '0', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<ColumnHeader type={this.props.type} />
|
||||||
|
<StatusListContainer type={this.props.type} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Column;
|
|
@ -0,0 +1,15 @@
|
||||||
|
const ColumnHeader = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
type: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '15px', fontSize: '16px', background: '#2f3441', flex: '0 0 auto' }}>
|
||||||
|
{this.props.type}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ColumnHeader;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Column from './column';
|
||||||
|
|
||||||
|
const ColumnsArea = React.createClass({
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', flex: '1' }}>
|
||||||
|
<Column type='home' />
|
||||||
|
<Column type='mentions' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ColumnsArea;
|
|
@ -0,0 +1,16 @@
|
||||||
|
import NavBar from './nav_bar';
|
||||||
|
import ColumnsArea from './columns_area';
|
||||||
|
|
||||||
|
const Frontend = React.createClass({
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div style={{ flex: '0 0 auto', display: 'flex', width: '100%', height: '100%', background: '#1a1c23' }}>
|
||||||
|
<NavBar />
|
||||||
|
<ColumnsArea />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Frontend;
|
|
@ -0,0 +1,8 @@
|
||||||
|
const NavBar = React.createClass({
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return <div style={{ background: '#2f3441', width: '60px', margin: '10px', marginRight: '0' }} />;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default NavBar;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
const Status = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
status: ImmutablePropTypes.map.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
console.log(this.props.status.toJS());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100px' }}>
|
||||||
|
{this.props.status.getIn(['account', 'username'])}: {this.props.status.get('content')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Status;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Status from './status';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
const StatusList = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
statuses: ImmutablePropTypes.list.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div style={{ overflowY: 'scroll', flex: '1 1 auto' }}>
|
||||||
|
<div>
|
||||||
|
{this.props.statuses.map((status) => {
|
||||||
|
return <Status key={status.get('id')} status={status} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default StatusList;
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import configureStore from '../store/configureStore';
|
||||||
|
import Frontend from '../components/frontend';
|
||||||
|
import { setTimeline, addStatus } from '../actions/statuses';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
const Root = React.createClass({
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
for (var timelineType in this.props.timelines) {
|
||||||
|
if (this.props.timelines.hasOwnProperty(timelineType)) {
|
||||||
|
store.dispatch(setTimeline(timelineType, JSON.parse(this.props.timelines[timelineType])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof App !== 'undefined') {
|
||||||
|
App.timeline = App.cable.subscriptions.create("TimelineChannel", {
|
||||||
|
connected: function() {},
|
||||||
|
|
||||||
|
disconnected: function() {},
|
||||||
|
|
||||||
|
received: function(data) {
|
||||||
|
return store.dispatch(addStatus(data.timeline, JSON.parse(data.message)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<Frontend />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Root;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import StatusList from '../components/status_list';
|
||||||
|
|
||||||
|
const mapStateToProps = function (state, props) {
|
||||||
|
return {
|
||||||
|
statuses: state.getIn(['statuses', props.type])
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(StatusList);
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { combineReducers } from 'redux-immutable';
|
||||||
|
import statuses from './statuses';
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
statuses
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { SET_TIMELINE, ADD_STATUS } from '../actions/statuses';
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
|
const initialState = Immutable.Map();
|
||||||
|
|
||||||
|
export default function statuses(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case SET_TIMELINE:
|
||||||
|
return state.set(action.timeline, Immutable.fromJS(action.statuses));
|
||||||
|
case ADD_STATUS:
|
||||||
|
return state.update(action.timeline, function (list) {
|
||||||
|
list.unshift(Immutable.fromJS(action.status));
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import appReducer from '../reducers';
|
||||||
|
|
||||||
|
export default function configureStore(initialState) {
|
||||||
|
return createStore(appReducer, initialState);
|
||||||
|
}
|
|
@ -67,6 +67,23 @@ body {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding-bottom: 140px;
|
padding-bottom: 140px;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
font-feature-settings: "kern";
|
||||||
|
|
||||||
|
&.app-body {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-holder {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|
|
@ -12,6 +12,8 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
helper_method :current_account
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
class HomeController < ApplicationController
|
class HomeController < ApplicationController
|
||||||
layout 'dashboard'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@timeline = Feed.new(:home, current_user.account).get(10, params[:max_id])
|
@body_classes = 'app-body'
|
||||||
|
@home = Feed.new(:home, current_user.account).get(20)
|
||||||
|
@mentions = Feed.new(:mentions, current_user.account).get(20)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
class SettingsController < ApplicationController
|
class SettingsController < ApplicationController
|
||||||
layout 'dashboard'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
class StatusesController < ApplicationController
|
class StatusesController < ApplicationController
|
||||||
layout 'dashboard'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Feed
|
||||||
@account = account
|
@account = account
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(limit, max_id)
|
def get(limit, max_id = nil)
|
||||||
max_id = '+inf' if max_id.nil?
|
max_id = '+inf' if max_id.nil?
|
||||||
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", '-inf', limit: [0, limit])
|
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", '-inf', limit: [0, limit])
|
||||||
status_map = Hash.new
|
status_map = Hash.new
|
||||||
|
|
|
@ -31,7 +31,7 @@ class FanOutOnWriteService < BaseService
|
||||||
def push(type, receiver, status)
|
def push(type, receiver, status)
|
||||||
redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id)
|
redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id)
|
||||||
trim(type, receiver)
|
trim(type, receiver)
|
||||||
ActionCable.server.broadcast("timeline:#{receiver.id}", message: inline_render(receiver, status))
|
ActionCable.server.broadcast("timeline:#{receiver.id}", timeline: type, message: inline_render(receiver, status))
|
||||||
end
|
end
|
||||||
|
|
||||||
def trim(type, receiver)
|
def trim(type, receiver)
|
||||||
|
|
|
@ -1,10 +1 @@
|
||||||
= simple_form_for Status.new, url: statuses_path, method: :post do |f|
|
= react_component 'Root', { timelines: { home: render(file: 'api/statuses/home', locals: { statuses: @home }, formats: :json), mentions: render(file: 'api/statuses/mentions', locals: { statuses: @mentions }, formats: :json) }}, class: 'app-holder', prerender: false
|
||||||
= f.input :text, required: true, autofocus: true, label: false, placeholder: 'What are you up to?'
|
|
||||||
|
|
||||||
.form-actions
|
|
||||||
= f.button :submit, 'Post update'
|
|
||||||
|
|
||||||
- content_for :raw_content do
|
|
||||||
.activity-stream.activity-stream-embedded
|
|
||||||
- @timeline.each do |status|
|
|
||||||
= render partial: 'stream_entries/status', locals: { status: status }
|
|
||||||
|
|
|
@ -9,5 +9,5 @@
|
||||||
= javascript_include_tag 'application'
|
= javascript_include_tag 'application'
|
||||||
= csrf_meta_tags
|
= csrf_meta_tags
|
||||||
= yield :header_tags
|
= yield :header_tags
|
||||||
%body
|
%body{ class: @body_classes }
|
||||||
= content_for?(:content) ? yield(:content) : yield
|
= content_for?(:content) ? yield(:content) : yield
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
- content_for :content do
|
|
||||||
.dashboard-wrapper
|
|
||||||
.dashboard__sidebar
|
|
||||||
.dashboard__top-bar.alternate
|
|
||||||
|
|
||||||
.dashboard__current-user
|
|
||||||
= link_to account_path(current_user.account) do
|
|
||||||
= image_tag current_user.account.avatar.url(:medium), class: 'dashboard__current-user__avatar'
|
|
||||||
%strong.dashboard__current-user__display-name= display_name(current_user.account)
|
|
||||||
%span.dashboard__current-user__username= "@#{current_user.account.username}"
|
|
||||||
%ul
|
|
||||||
%li{ class: active_nav_class(root_path) }
|
|
||||||
= link_to root_path do
|
|
||||||
= fa_icon 'home'
|
|
||||||
Home
|
|
||||||
%li{ class: active_nav_class(oauth_authorized_applications_path) }
|
|
||||||
= link_to oauth_authorized_applications_path do
|
|
||||||
= fa_icon 'shield'
|
|
||||||
Authorized apps
|
|
||||||
%li{ class: active_nav_class(settings_path) }
|
|
||||||
= link_to settings_path do
|
|
||||||
= fa_icon 'user'
|
|
||||||
Edit profile
|
|
||||||
|
|
||||||
.dashboard__content
|
|
||||||
.dashboard__top-bar
|
|
||||||
= content_for?(:page_title) ? yield(:page_title) : 'Mastodon'
|
|
||||||
%ul
|
|
||||||
%li= link_to fa_icon('gear'), edit_registration_path(current_user), title: 'Change password'
|
|
||||||
%li= link_to fa_icon('sign-out'), destroy_user_session_path, method: :delete, title: 'Sign out'
|
|
||||||
|
|
||||||
.dashboard__content__content= yield
|
|
||||||
|
|
||||||
= yield(:raw_content)
|
|
||||||
|
|
||||||
.footer
|
|
||||||
.domain= Rails.configuration.x.local_domain
|
|
||||||
|
|
||||||
= render template: "layouts/application"
|
|
|
@ -28,12 +28,14 @@ module Mastodon
|
||||||
config.active_job.queue_adapter = :sidekiq
|
config.active_job.queue_adapter = :sidekiq
|
||||||
|
|
||||||
config.to_prepare do
|
config.to_prepare do
|
||||||
Doorkeeper::ApplicationsController.layout 'dashboard'
|
# Doorkeeper::ApplicationsController.layout 'dashboard'
|
||||||
Doorkeeper::AuthorizedApplicationsController.layout 'dashboard'
|
# Doorkeeper::AuthorizedApplicationsController.layout 'dashboard'
|
||||||
Doorkeeper::AuthorizationsController.layout 'auth'
|
Doorkeeper::AuthorizationsController.layout 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
config.middleware.use Rack::Attack
|
config.middleware.use Rack::Attack
|
||||||
config.middleware.use Rack::Deflater
|
config.middleware.use Rack::Deflater
|
||||||
|
|
||||||
|
config.browserify_rails.commandline_options = "--transform [ babelify --presets [ es2015 react ] ] --extension=\".jsx\""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -63,6 +63,8 @@ Rails.application.configure do
|
||||||
Bullet.bullet_logger = true
|
Bullet.bullet_logger = true
|
||||||
Bullet.rails_logger = true
|
Bullet.rails_logger = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.react.variant = :development
|
||||||
end
|
end
|
||||||
|
|
||||||
require 'sidekiq/testing'
|
require 'sidekiq/testing'
|
||||||
|
|
|
@ -80,4 +80,6 @@ Rails.application.configure do
|
||||||
}
|
}
|
||||||
|
|
||||||
config.action_mailer.delivery_method = :smtp
|
config.action_mailer.delivery_method = :smtp
|
||||||
|
|
||||||
|
config.react.variant = :production
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "mastodon",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-preset-es2015": "^6.13.2",
|
||||||
|
"babel-preset-react": "^6.11.1",
|
||||||
|
"babelify": "^7.3.0",
|
||||||
|
"browserify": "^13.1.0",
|
||||||
|
"browserify-incremental": "^3.1.1",
|
||||||
|
"react": "^15.3.0",
|
||||||
|
"react-dom": "^15.3.0",
|
||||||
|
"redux-devtools": "^3.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"immutable": "^3.8.1",
|
||||||
|
"react-immutable-proptypes": "^2.1.0",
|
||||||
|
"react-redux": "^4.4.5",
|
||||||
|
"redux": "^3.5.2",
|
||||||
|
"redux-immutable": "^3.0.8"
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue