├── .foreman
├── .gitignore
├── .rspec
├── .ruby-gemset
├── .ruby-version
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Procfile
├── Procfile.dev
├── README.md
├── Rakefile
├── app
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── concerns
│ └── model_filter.rb
├── controllers
│ ├── application_controller.rb
│ ├── authorized_controller.rb
│ ├── categories_controller.rb
│ ├── category_controller.rb
│ ├── comments_controller.rb
│ ├── concerns
│ │ └── .keep
│ ├── customers_controller.rb
│ ├── employees_controller.rb
│ ├── orders_controller.rb
│ ├── posts_controller.rb
│ ├── products_controller.rb
│ ├── roles_controller.rb
│ ├── suppliers_controller.rb
│ └── users_controller.rb
├── jobs
│ └── application_job.rb
├── mailers
│ └── application_mailer.rb
├── models
│ ├── ability.rb
│ ├── application_record.rb
│ ├── category.rb
│ ├── comment.rb
│ ├── concerns
│ │ └── .keep
│ ├── customer.rb
│ ├── employee.rb
│ ├── order.rb
│ ├── post.rb
│ ├── product.rb
│ ├── role.rb
│ ├── supplier.rb
│ └── user.rb
├── resources
│ ├── category_resource.rb
│ ├── comment_resource.rb
│ ├── customer_resource.rb
│ ├── employee_resource.rb
│ ├── order_resource.rb
│ ├── post_resource.rb
│ ├── product_resource.rb
│ ├── role_resource.rb
│ ├── supplier_resource.rb
│ └── user_resource.rb
└── views
│ └── layouts
│ ├── mailer.html.erb
│ └── mailer.text.erb
├── bin
├── bundle
├── rails
├── rake
├── setup
├── spring
└── update
├── client
├── .babelrc
├── .editorconfig
├── .eslintrc.yml
├── .gitignore
├── .nvmrc
├── README.md
├── assets
│ └── favicon.ico
├── package.json
├── src
│ ├── api
│ │ ├── __snapshots__
│ │ │ └── normalize.spec.js.snap
│ │ ├── client.js
│ │ ├── index.js
│ │ ├── normalize.js
│ │ └── normalize.spec.js
│ ├── components
│ │ ├── App.js
│ │ ├── Auth
│ │ │ ├── Login.js
│ │ │ ├── LoginForm.js
│ │ │ └── index.js
│ │ ├── Categories
│ │ │ ├── CategoryForm.js
│ │ │ ├── CategoryList.js
│ │ │ └── index.js
│ │ ├── Customers
│ │ │ ├── CustomerEdit.js
│ │ │ ├── CustomerForm.js
│ │ │ ├── CustomerList.js
│ │ │ ├── CustomerListFilter.js
│ │ │ └── index.js
│ │ ├── Dashboard.js
│ │ ├── Orders
│ │ │ ├── OrderEdit.js
│ │ │ ├── OrderForm.js
│ │ │ ├── OrderList.js
│ │ │ └── index.js
│ │ ├── Posts
│ │ │ ├── PostEdit.js
│ │ │ ├── PostForm.js
│ │ │ ├── PostList.js
│ │ │ ├── PostListFilter.js
│ │ │ └── index.js
│ │ ├── Products
│ │ │ ├── ProductEdit.js
│ │ │ ├── ProductForm.js
│ │ │ ├── ProductList.js
│ │ │ └── index.js
│ │ ├── Routes.js
│ │ ├── UI
│ │ │ ├── CardSingle.js
│ │ │ ├── EditHeader.js
│ │ │ ├── ErrorAlert.js
│ │ │ ├── ListTable.js
│ │ │ ├── Loading.js
│ │ │ ├── Pagination.js
│ │ │ └── index.js
│ │ └── Users
│ │ │ ├── UserEdit.js
│ │ │ ├── UserForm.js
│ │ │ ├── UserList.js
│ │ │ └── index.js
│ ├── forms
│ │ ├── fields
│ │ │ ├── InputField.js
│ │ │ ├── MultiselectField.js
│ │ │ ├── SelectField.js
│ │ │ └── TextArea.js
│ │ ├── index.js
│ │ └── validations
│ │ │ └── required.js
│ ├── hocs
│ │ ├── index.js
│ │ ├── withResource.js
│ │ └── withResourceList.js
│ ├── index.css
│ ├── index.ejs
│ ├── index.js
│ └── store
│ │ ├── api
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── selectors.js
│ │ ├── auth
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── selectors.js
│ │ ├── configureStore.js
│ │ └── utils.js
├── webpack.config.js
└── yarn.lock
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── application_controller_renderer.rb
│ ├── backtrace_silencers.rb
│ ├── cors.rb
│ ├── devise_token_auth.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── jsonapi_resources.rb
│ ├── mime_types.rb
│ ├── new_framework_defaults.rb
│ ├── rolify.rb
│ └── wrap_parameters.rb
├── locales
│ └── en.yml
├── puma.rb
├── routes.rb
├── secrets.yml
└── spring.rb
├── db
├── migrate
│ ├── 20170328190422_create_categories.rb
│ ├── 20170328194723_create_posts.rb
│ ├── 20170328194726_create_comments.rb
│ ├── 20170418135338_devise_token_auth_create_users.rb
│ ├── 20170504204400_add_post_parts.rb
│ ├── 20170508090812_rolify_create_roles.rb
│ ├── 20170516130238_create_orders.rb
│ ├── 20170516130257_create_products.rb
│ ├── 20170516130511_create_customers.rb
│ ├── 20170516130923_add_employee_table.rb
│ └── 20170516131023_create_suppliers.rb
├── seeds.rb
└── structure.sql
├── lib
└── tasks
│ └── .keep
├── log
└── .keep
├── package.json
├── server
└── tmp
│ └── restart.txt
├── spec
├── factories
│ ├── category.rb
│ ├── comment.rb
│ ├── customer.rb
│ ├── employee.rb
│ ├── order.rb
│ ├── post.rb
│ ├── product.rb
│ └── supplier.rb
├── rails_helper.rb
└── spec_helper.rb
├── test
├── controllers
│ ├── category_controller_test.rb
│ └── employee_controller_test.rb
├── fixtures
│ └── roles.yml
└── models
│ └── role_test.rb
├── tmp
└── restart.txt
└── yarn.lock
/.foreman:
--------------------------------------------------------------------------------
1 | procfile: Procfile.dev
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore all logfiles and tempfiles.
11 | /log/*
12 | /tmp/*
13 | !/log/.keep
14 | !/tmp/.keep
15 |
16 | # Ignore Byebug command history file.
17 | .byebug_history
18 | .idea/
19 | public/
20 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | rails-json-api
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.3.1
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | git_source(:github) do |repo_name|
4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
5 | "https://github.com/#{repo_name}.git"
6 | end
7 |
8 |
9 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
10 | gem 'rails', '~> 5.0.2'
11 | # Use postgresql as the database for Active Record
12 | gem 'pg', '~> 0.18'
13 | # Use Puma as the app server
14 | gem 'puma', '~> 3.0'
15 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
16 | # gem 'jbuilder', '~> 2.5'
17 | # Use Redis adapter to run Action Cable in production
18 | # gem 'redis', '~> 3.0'
19 | # Use ActiveModel has_secure_password
20 | # gem 'bcrypt', '~> 3.1.7'
21 |
22 | # Use Capistrano for deployment
23 | # gem 'capistrano-rails', group: :development
24 |
25 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
26 | gem 'rack-cors'
27 | gem 'jsonapi-resources'
28 | gem 'factory_girl'
29 | gem 'faker'
30 | gem 'devise_token_auth'
31 | gem 'cancan'
32 | gem 'rolify'
33 | gem 'pry'
34 |
35 | group :development, :test do
36 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
37 | gem 'byebug', platform: :mri
38 | gem 'foreman'
39 | end
40 |
41 | group :test do
42 | gem 'rspec-rails'
43 | end
44 |
45 | group :development do
46 | gem 'listen', '~> 3.0.5'
47 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
48 | gem 'spring'
49 | gem 'spring-watcher-listen', '~> 2.0.0'
50 | end
51 |
52 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
53 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
54 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (5.0.2)
5 | actionpack (= 5.0.2)
6 | nio4r (>= 1.2, < 3.0)
7 | websocket-driver (~> 0.6.1)
8 | actionmailer (5.0.2)
9 | actionpack (= 5.0.2)
10 | actionview (= 5.0.2)
11 | activejob (= 5.0.2)
12 | mail (~> 2.5, >= 2.5.4)
13 | rails-dom-testing (~> 2.0)
14 | actionpack (5.0.2)
15 | actionview (= 5.0.2)
16 | activesupport (= 5.0.2)
17 | rack (~> 2.0)
18 | rack-test (~> 0.6.3)
19 | rails-dom-testing (~> 2.0)
20 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
21 | actionview (5.0.2)
22 | activesupport (= 5.0.2)
23 | builder (~> 3.1)
24 | erubis (~> 2.7.0)
25 | rails-dom-testing (~> 2.0)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
27 | activejob (5.0.2)
28 | activesupport (= 5.0.2)
29 | globalid (>= 0.3.6)
30 | activemodel (5.0.2)
31 | activesupport (= 5.0.2)
32 | activerecord (5.0.2)
33 | activemodel (= 5.0.2)
34 | activesupport (= 5.0.2)
35 | arel (~> 7.0)
36 | activesupport (5.0.2)
37 | concurrent-ruby (~> 1.0, >= 1.0.2)
38 | i18n (~> 0.7)
39 | minitest (~> 5.1)
40 | tzinfo (~> 1.1)
41 | arel (7.1.4)
42 | bcrypt (3.1.11)
43 | builder (3.2.3)
44 | byebug (9.0.6)
45 | cancan (1.6.10)
46 | coderay (1.1.1)
47 | concurrent-ruby (1.0.5)
48 | devise (4.2.0)
49 | bcrypt (~> 3.0)
50 | orm_adapter (~> 0.1)
51 | railties (>= 4.1.0, < 5.1)
52 | responders
53 | warden (~> 1.2.3)
54 | devise_token_auth (0.1.40)
55 | devise (> 3.5.2, <= 4.2)
56 | rails (< 6)
57 | diff-lcs (1.3)
58 | erubis (2.7.0)
59 | factory_girl (4.8.0)
60 | activesupport (>= 3.0.0)
61 | faker (1.7.3)
62 | i18n (~> 0.5)
63 | ffi (1.9.18)
64 | foreman (0.82.0)
65 | thor (~> 0.19.1)
66 | globalid (0.3.7)
67 | activesupport (>= 4.1.0)
68 | i18n (0.8.1)
69 | jsonapi-resources (0.9.0)
70 | activerecord (>= 4.1)
71 | concurrent-ruby
72 | railties (>= 4.1)
73 | listen (3.0.8)
74 | rb-fsevent (~> 0.9, >= 0.9.4)
75 | rb-inotify (~> 0.9, >= 0.9.7)
76 | loofah (2.0.3)
77 | nokogiri (>= 1.5.9)
78 | mail (2.6.4)
79 | mime-types (>= 1.16, < 4)
80 | method_source (0.8.2)
81 | mime-types (3.1)
82 | mime-types-data (~> 3.2015)
83 | mime-types-data (3.2016.0521)
84 | mini_portile2 (2.1.0)
85 | minitest (5.10.1)
86 | nio4r (2.0.0)
87 | nokogiri (1.7.1)
88 | mini_portile2 (~> 2.1.0)
89 | orm_adapter (0.5.0)
90 | pg (0.20.0)
91 | pry (0.10.4)
92 | coderay (~> 1.1.0)
93 | method_source (~> 0.8.1)
94 | slop (~> 3.4)
95 | puma (3.8.2)
96 | rack (2.0.1)
97 | rack-cors (0.4.1)
98 | rack-test (0.6.3)
99 | rack (>= 1.0)
100 | rails (5.0.2)
101 | actioncable (= 5.0.2)
102 | actionmailer (= 5.0.2)
103 | actionpack (= 5.0.2)
104 | actionview (= 5.0.2)
105 | activejob (= 5.0.2)
106 | activemodel (= 5.0.2)
107 | activerecord (= 5.0.2)
108 | activesupport (= 5.0.2)
109 | bundler (>= 1.3.0, < 2.0)
110 | railties (= 5.0.2)
111 | sprockets-rails (>= 2.0.0)
112 | rails-dom-testing (2.0.2)
113 | activesupport (>= 4.2.0, < 6.0)
114 | nokogiri (~> 1.6)
115 | rails-html-sanitizer (1.0.3)
116 | loofah (~> 2.0)
117 | railties (5.0.2)
118 | actionpack (= 5.0.2)
119 | activesupport (= 5.0.2)
120 | method_source
121 | rake (>= 0.8.7)
122 | thor (>= 0.18.1, < 2.0)
123 | rake (12.0.0)
124 | rb-fsevent (0.9.8)
125 | rb-inotify (0.9.8)
126 | ffi (>= 0.5.0)
127 | responders (2.3.0)
128 | railties (>= 4.2.0, < 5.1)
129 | rolify (5.1.0)
130 | rspec-core (3.5.4)
131 | rspec-support (~> 3.5.0)
132 | rspec-expectations (3.5.0)
133 | diff-lcs (>= 1.2.0, < 2.0)
134 | rspec-support (~> 3.5.0)
135 | rspec-mocks (3.5.0)
136 | diff-lcs (>= 1.2.0, < 2.0)
137 | rspec-support (~> 3.5.0)
138 | rspec-rails (3.5.2)
139 | actionpack (>= 3.0)
140 | activesupport (>= 3.0)
141 | railties (>= 3.0)
142 | rspec-core (~> 3.5.0)
143 | rspec-expectations (~> 3.5.0)
144 | rspec-mocks (~> 3.5.0)
145 | rspec-support (~> 3.5.0)
146 | rspec-support (3.5.0)
147 | slop (3.6.0)
148 | spring (2.0.1)
149 | activesupport (>= 4.2)
150 | spring-watcher-listen (2.0.1)
151 | listen (>= 2.7, < 4.0)
152 | spring (>= 1.2, < 3.0)
153 | sprockets (3.7.1)
154 | concurrent-ruby (~> 1.0)
155 | rack (> 1, < 3)
156 | sprockets-rails (3.2.0)
157 | actionpack (>= 4.0)
158 | activesupport (>= 4.0)
159 | sprockets (>= 3.0.0)
160 | thor (0.19.4)
161 | thread_safe (0.3.6)
162 | tzinfo (1.2.3)
163 | thread_safe (~> 0.1)
164 | warden (1.2.7)
165 | rack (>= 1.0)
166 | websocket-driver (0.6.5)
167 | websocket-extensions (>= 0.1.0)
168 | websocket-extensions (0.1.2)
169 |
170 | PLATFORMS
171 | ruby
172 |
173 | DEPENDENCIES
174 | byebug
175 | cancan
176 | devise_token_auth
177 | factory_girl
178 | faker
179 | foreman
180 | jsonapi-resources
181 | listen (~> 3.0.5)
182 | pg (~> 0.18)
183 | pry
184 | puma (~> 3.0)
185 | rack-cors
186 | rails (~> 5.0.2)
187 | rolify
188 | rspec-rails
189 | spring
190 | spring-watcher-listen (~> 2.0.0)
191 | tzinfo-data
192 |
193 | BUNDLED WITH
194 | 1.14.6
195 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-2019 Tomasz Bąk https://www.tomaszbak.com/
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec puma -p $PORT
2 |
--------------------------------------------------------------------------------
/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: bundle exec puma -p 3001
2 | webpack: cd client && npm start
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [rails-json-api-react](https://github.com/tb/rails-json-api-react)
2 |
3 | by [React Developers @ Selleo](https://selleo.com/react-expert-developers-team)
4 |
5 | ## [DEMO](https://rails-json-api-react.herokuapp.com)
6 |
7 | Demo user: user1@example.com / Secret123
8 |
9 | ## Setup app
10 |
11 | bundle
12 | rake db:setup
13 | cd client && yarn
14 |
15 | ## Start app
16 |
17 | foreman start
18 | open http://localhost:3000
19 |
20 | ## Adding new JSON API resource
21 |
22 | rails g model category name:string
23 | rails generate jsonapi:resource category
24 | rails g controller Category --skip-assets
25 |
26 | ### routes.rb
27 |
28 | jsonapi_resources :categories
29 |
30 | ### Client
31 |
32 | Add list, edit and form components in `client/src/components/` based on one of existing.
33 |
34 | ### License
35 |
36 | MIT
37 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/concerns/model_filter.rb:
--------------------------------------------------------------------------------
1 | # see https://github.com/cerebris/jsonapi-resources/issues/460
2 | module ModelFilter
3 | def model_filter(name, opts = {})
4 | opts[:apply] = ->(records, value, _options) do
5 | records.public_send(name, value)
6 | end
7 |
8 | filter name, opts
9 | end
10 |
11 | def model_filters(*names)
12 | names.each { |name| model_filter(name, names.extract_options!) }
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | # protect_from_forgery with: :null_session
5 | #
6 | rescue_from CanCan::AccessDenied do |exception|
7 | render json: { message: "You don't have permissions." }, status: :forbidden
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/authorized_controller.rb:
--------------------------------------------------------------------------------
1 | class AuthorizedController < ActionController::Base
2 | include DeviseTokenAuth::Concerns::SetUserByToken
3 | include JSONAPI::ActsAsResourceController
4 | before_action :authenticate_user!
5 | end
6 |
--------------------------------------------------------------------------------
/app/controllers/categories_controller.rb:
--------------------------------------------------------------------------------
1 | class CategoriesController < AuthorizedController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/category_controller.rb:
--------------------------------------------------------------------------------
1 | class CategoryController < ApplicationController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/comments_controller.rb:
--------------------------------------------------------------------------------
1 | class CommentsController < AuthorizedController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tb/rails-json-api-react/b2c3363e5e372ae6876a3a900d3f2ca3c2869f46/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/customers_controller.rb:
--------------------------------------------------------------------------------
1 | class CustomersController < AuthorizedController
2 |
3 | end
4 |
--------------------------------------------------------------------------------
/app/controllers/employees_controller.rb:
--------------------------------------------------------------------------------
1 | class EmployeesController < AuthorizedController
2 |
3 | end
4 |
--------------------------------------------------------------------------------
/app/controllers/orders_controller.rb:
--------------------------------------------------------------------------------
1 | class OrdersController < AuthorizedController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class PostsController < AuthorizedController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < AuthorizedController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/roles_controller.rb:
--------------------------------------------------------------------------------
1 | class RolesController < AuthorizedController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/suppliers_controller.rb:
--------------------------------------------------------------------------------
1 | class SuppliersController < AuthorizedController
2 | end
3 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < AuthorizedController
2 | load_and_authorize_resource
3 | end
4 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/ability.rb:
--------------------------------------------------------------------------------
1 | class Ability
2 | include CanCan::Ability
3 |
4 | def initialize(user)
5 | user ||= User.new # guest user (not logged in)
6 | if user.is_admin?
7 | can :manage, :all
8 | else
9 | can :manage, Post
10 | can :manage, Category
11 | can :manage, Comment
12 | can :update, User, id: user.id
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/category.rb:
--------------------------------------------------------------------------------
1 | class Category < ApplicationRecord
2 | has_many :posts, dependent: :nullify
3 |
4 | validates :name, presence: true, uniqueness: true
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/comment.rb:
--------------------------------------------------------------------------------
1 | class Comment < ApplicationRecord
2 | belongs_to :post
3 |
4 | validates :body, presence: true
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tb/rails-json-api-react/b2c3363e5e372ae6876a3a900d3f2ca3c2869f46/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/customer.rb:
--------------------------------------------------------------------------------
1 | class Customer < ApplicationRecord
2 | scope :company_name_contains, -> (value) { where('company_name ILIKE ?', "%#{value.join}%") }
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/employee.rb:
--------------------------------------------------------------------------------
1 | class Employee < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/order.rb:
--------------------------------------------------------------------------------
1 | class Order < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/post.rb:
--------------------------------------------------------------------------------
1 | class Post < ApplicationRecord
2 | belongs_to :category
3 | has_many :comments, dependent: :destroy
4 |
5 | validates :title, presence: true, uniqueness: true
6 |
7 | scope :title_contains, -> (value) { where('title ILIKE ?', "%#{value.join}%") }
8 | end
9 |
--------------------------------------------------------------------------------
/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | validates :product_name, presence: true, uniqueness: true
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/role.rb:
--------------------------------------------------------------------------------
1 | class Role < ApplicationRecord
2 | has_and_belongs_to_many :users, :join_table => :users_roles
3 |
4 | belongs_to :resource,
5 | :polymorphic => true,
6 | :optional => true
7 |
8 | validates :resource_type,
9 | :inclusion => { :in => Rolify.resource_types },
10 | :allow_nil => true
11 |
12 | scopify
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/supplier.rb:
--------------------------------------------------------------------------------
1 | class Supplier < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | rolify
3 | has_and_belongs_to_many :roles, :join_table => :users_roles
4 |
5 | # Include default devise modules.
6 | devise :database_authenticatable, :registerable,
7 | :recoverable, :rememberable, :trackable, :validatable,
8 | :confirmable
9 | include DeviseTokenAuth::Concerns::User
10 |
11 | scope :email_contains, -> (value) { where('email ILIKE ?', "%#{value.join}%") }
12 |
13 | def token_validation_response
14 | self.as_json(except: [
15 | :tokens, :created_at, :updated_at
16 | ]).merge(roles: self.roles.map(&:name))
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/resources/category_resource.rb:
--------------------------------------------------------------------------------
1 | class CategoryResource < JSONAPI::Resource
2 | attributes :name
3 |
4 | has_many :posts
5 | end
6 |
--------------------------------------------------------------------------------
/app/resources/comment_resource.rb:
--------------------------------------------------------------------------------
1 | class CommentResource < JSONAPI::Resource
2 | attributes :body
3 |
4 | has_one :post
5 | end
6 |
--------------------------------------------------------------------------------
/app/resources/customer_resource.rb:
--------------------------------------------------------------------------------
1 | class CustomerResource < JSONAPI::Resource
2 | extend ModelFilter
3 | attributes :company_name,
4 | :contact_name,
5 | :contact_title,
6 | :address,
7 | :city,
8 | :region,
9 | :postal_code,
10 | :country,
11 | :phone,
12 | :fax,
13 | :created_at
14 |
15 | paginator :paged
16 | model_filters :company_name_contains
17 | end
18 |
--------------------------------------------------------------------------------
/app/resources/employee_resource.rb:
--------------------------------------------------------------------------------
1 | class EmployeeResource < JSONAPI::Resource
2 | attributes :title, :created_at, :first_name
3 | end
4 |
--------------------------------------------------------------------------------
/app/resources/order_resource.rb:
--------------------------------------------------------------------------------
1 | class OrderResource < JSONAPI::Resource
2 | attributes :order_date, :required_date, :shipped_date, :ship_via, :freight, :ship_name, :ship_address, :ship_city,
3 | :ship_region, :ship_postal_code, :ship_country
4 | end
5 |
--------------------------------------------------------------------------------
/app/resources/post_resource.rb:
--------------------------------------------------------------------------------
1 | class PostResource < JSONAPI::Resource
2 | extend ModelFilter
3 |
4 | attributes :title, :created_at, :parts
5 |
6 | has_many :comments
7 | has_one :category
8 |
9 | paginator :paged
10 |
11 | filters :category
12 | model_filters :title_contains
13 | end
14 |
--------------------------------------------------------------------------------
/app/resources/product_resource.rb:
--------------------------------------------------------------------------------
1 | class ProductResource < JSONAPI::Resource
2 | attributes :product_name, :created_at
3 |
4 | paginator :paged
5 | end
6 |
--------------------------------------------------------------------------------
/app/resources/role_resource.rb:
--------------------------------------------------------------------------------
1 | class RoleResource < JSONAPI::Resource
2 | attributes :name
3 | end
4 |
--------------------------------------------------------------------------------
/app/resources/supplier_resource.rb:
--------------------------------------------------------------------------------
1 | class SupplierResource < JSONAPI::Resource
2 | attributes :company_name,
3 | :contact_name,
4 | :contact_title,
5 | :address,
6 | :city,
7 | :region,
8 | :postal_code,
9 | :country,
10 | :phone,
11 | :fax,
12 | :home_page
13 | end
14 |
--------------------------------------------------------------------------------
/app/resources/user_resource.rb:
--------------------------------------------------------------------------------
1 | class UserResource < JSONAPI::Resource
2 | extend ModelFilter
3 | attributes :email, :confirmed_at, :created_at, :roles
4 |
5 | paginator :paged
6 | model_filters :email_contains
7 |
8 | def roles
9 | @model.roles.pluck(:name)
10 | end
11 |
12 | def roles=(roles)
13 | @model.roles.destroy_all
14 | roles.map do |role|
15 | @model.add_role role
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../config/application', __dir__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a starting point to setup your application.
15 | # Add necessary setup steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | # puts "\n== Copying sample files =="
22 | # unless File.exist?('config/database.yml')
23 | # cp 'config/database.yml.sample', 'config/database.yml'
24 | # end
25 |
26 | puts "\n== Preparing database =="
27 | system! 'bin/rails db:setup'
28 |
29 | puts "\n== Removing old logs and tempfiles =="
30 | system! 'bin/rails log:clear tmp:clear'
31 |
32 | puts "\n== Restarting application server =="
33 | system! 'bin/rails restart'
34 | end
35 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" }
12 | if spring
13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
14 | gem 'spring', spring.version
15 | require 'spring/binstub'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a way to update your development environment automatically.
15 | # Add necessary update steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | puts "\n== Updating database =="
22 | system! 'bin/rails db:migrate'
23 |
24 | puts "\n== Removing old logs and tempfiles =="
25 | system! 'bin/rails log:clear tmp:clear'
26 |
27 | puts "\n== Restarting application server =="
28 | system! 'bin/rails restart'
29 | end
30 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/client/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/client/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: react-app
3 |
4 | env:
5 | browser: true
6 | es6: true
7 | jest: true
8 | node: true
9 |
10 | globals:
11 | context: false
12 | jest: true
13 |
14 | rules:
15 | jsx-a11y/href-no-hash: 0
16 | jsx-a11y/alt-text: 0
17 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | public
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 | .idea
17 |
--------------------------------------------------------------------------------
/client/.nvmrc:
--------------------------------------------------------------------------------
1 | 6
2 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 | ## Install packages
4 |
5 | yarn
6 |
7 | ## Run tests
8 |
9 | yarn test
10 |
11 | ## Start dev server
12 |
13 | yarn start
14 |
15 | ## Upgrade packages
16 |
17 | yarn upgrade-interactive
18 |
--------------------------------------------------------------------------------
/client/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tb/rails-json-api-react/b2c3363e5e372ae6876a3a900d3f2ca3c2869f46/client/assets/favicon.ico
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "jest": {
6 | "snapshotSerializers": [
7 | "/node_modules/enzyme-to-json/serializer"
8 | ]
9 | },
10 | "scripts": {
11 | "build": "shx rm -rf ../public/** && NODE_ENV=production webpack -p --progress",
12 | "lint": "eslint --ext .js,.jsx src/",
13 | "lint:fix": "npm run lint -- --fix",
14 | "start": "webpack-dev-server",
15 | "test": "NODE_ENV=test jest"
16 | },
17 | "dependencies": {
18 | "axios": "^0.16.1",
19 | "babel-polyfill": "^6.23.0",
20 | "bootstrap": "^4.0.0-alpha.6",
21 | "jsonapi-serializer": "^3.5.2",
22 | "lodash": "^4.17.4",
23 | "object-path-immutable": "^0.5.1",
24 | "prop-types": "^15.5.10",
25 | "qs": "^6.4.0",
26 | "react": "^15.4.2",
27 | "react-addons-css-transition-group": "^15.5.2",
28 | "react-addons-transition-group": "^15.5.2",
29 | "react-dom": "^15.4.2",
30 | "react-redux": "^5.0.3",
31 | "react-router": "3.0.2",
32 | "react-router-redux": "^4.0.8",
33 | "react-ultimate-pagination": "^1.0.1",
34 | "reactstrap": "^4.5.0",
35 | "recompose": "^0.23.1",
36 | "redux": "^3.6.0",
37 | "redux-auth-wrapper": "^1.0.0",
38 | "redux-form": "^6.6.1",
39 | "redux-thunk": "^2.2.0"
40 | },
41 | "devDependencies": {
42 | "babel-eslint": "^7.2.3",
43 | "babel-loader": "^6.4.0",
44 | "babel-preset-es2015": "^6.22.0",
45 | "babel-preset-react": "^6.23.0",
46 | "babel-preset-stage-0": "^6.22.0",
47 | "copy-webpack-plugin": "^4.0.1",
48 | "css-loader": "^0.26.4",
49 | "enzyme": "^2.7.1",
50 | "enzyme-to-json": "^1.5.0",
51 | "eslint": "^3.19.0",
52 | "eslint-config-react-app": "^1.0.4",
53 | "eslint-plugin-flowtype": "^2.33.0",
54 | "eslint-plugin-import": "^2.3.0",
55 | "eslint-plugin-jsx-a11y": "^5.0.3",
56 | "eslint-plugin-react": "^7.0.1",
57 | "extract-text-webpack-plugin": "^2.1.0",
58 | "html-webpack-plugin": "^2.28.0",
59 | "jest": "^19.0.2",
60 | "node-sass": "^4.5.0",
61 | "react-test-renderer": "^15.5.4",
62 | "sass-loader": "^6.0.3",
63 | "shx": "^0.2.2",
64 | "style-loader": "^0.13.2",
65 | "webpack": "^2.2.1",
66 | "webpack-config-utils": "^2.3.0",
67 | "webpack-dev-server": "^2.4.1"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/client/src/api/__snapshots__/normalize.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`normalize categories denormalize 1`] = `
4 | Object {
5 | "data": Object {
6 | "attributes": Object {
7 | "name": "Category 11",
8 | },
9 | "id": "11",
10 | "type": "categories",
11 | },
12 | }
13 | `;
14 |
15 | exports[`normalize categories normalize 1`] = `
16 | Object {
17 | "id": "11",
18 | "name": "Category 11",
19 | }
20 | `;
21 |
22 | exports[`normalize posts denormalize 1`] = `
23 | Object {
24 | "data": Object {
25 | "attributes": Object {
26 | "body": "Body 1",
27 | "title": "Title 1",
28 | },
29 | "id": "1",
30 | "relationships": Object {
31 | "category": Object {
32 | "data": Object {
33 | "id": "11",
34 | "type": "categories",
35 | },
36 | },
37 | },
38 | "type": "posts",
39 | },
40 | }
41 | `;
42 |
43 | exports[`normalize posts normalize 1`] = `
44 | Object {
45 | "body": "Body 1",
46 | "category": Object {
47 | "id": "11",
48 | "name": undefined,
49 | },
50 | "id": "1",
51 | "title": "Title 1",
52 | }
53 | `;
54 |
--------------------------------------------------------------------------------
/client/src/api/client.js:
--------------------------------------------------------------------------------
1 | import qs from 'qs';
2 | import axios from 'axios';
3 | import {
4 | castArray,
5 | get,
6 | groupBy,
7 | keys,
8 | set,
9 | values,
10 | zipObject,
11 | } from 'lodash';
12 |
13 | import { normalize } from './normalize';
14 |
15 | export { denormalize } from './normalize';
16 |
17 | export const client = axios.create({
18 | baseURL: '/',
19 | headers: {
20 | Accept: 'application/vnd.api+json',
21 | 'Content-Type': 'application/vnd.api+json',
22 | },
23 | paramsSerializer: params => qs.stringify(params, { format: 'RFC1738', arrayFormat: 'brackets' }),
24 | });
25 |
26 | client.interceptors.response.use(
27 | response => response,
28 | (error) => {
29 | if (error.response.status === 401) {
30 | window.location.href = '/#/login';
31 | }
32 | return Promise.reject(error);
33 | },
34 | );
35 |
36 | client.interceptors.request.use(
37 | (config) => {
38 | const user = JSON.parse(localStorage.getItem('user') || '{}');
39 | if (user['access-token']) {
40 | config.headers['x-jwt-token'] = 'Bearer';
41 | config.headers.client = user.client;
42 | config.headers['access-token'] = user['access-token'];
43 | config.headers.uid = user.uid;
44 | }
45 | return config;
46 | },
47 | error => Promise.reject(error),
48 | );
49 |
50 | export const normalizeResponse = (response) => {
51 | const { data = [], included = [] } = response.data;
52 | const dataByType = groupBy(castArray(data).concat(included), 'type');
53 |
54 | const normalizeItems = (items = []) => Promise.all(items.map(item =>
55 | normalize(item.type, { data: item, included }),
56 | ));
57 |
58 | return Promise.all(values(dataByType).map(normalizeItems))
59 | .then(normalizedItems => ({
60 | ...response.data,
61 | normalized: zipObject(keys(dataByType), normalizedItems),
62 | }));
63 | };
64 |
65 | export const normalizeEndpointError = (err) => {
66 | const error = get(err, 'response.data.errors[0]');
67 | // eslint-disable-next-line no-throw-literal
68 | throw { message: error.detail || error.title || err.message };
69 | };
70 |
71 | export const normalizeErrors = (err) => {
72 | throw get(err, 'response.data.errors')
73 | .reduce((errors, error) => {
74 | const attribute = /\/data\/[a-z]*\/(.*)$/.exec(get(error, 'source.pointer'))[1];
75 | set(errors, attribute.split('/'), error.title);
76 | return errors;
77 | }, {});
78 | };
79 |
--------------------------------------------------------------------------------
/client/src/api/index.js:
--------------------------------------------------------------------------------
1 | export * from './client';
2 |
--------------------------------------------------------------------------------
/client/src/api/normalize.js:
--------------------------------------------------------------------------------
1 | import { omit } from 'lodash';
2 | import { Deserializer, Serializer } from 'jsonapi-serializer';
3 |
4 | const serializers = {
5 | categories: {
6 | serializer: new Serializer('categories', {
7 | keyForAttribute: 'camelCase',
8 | attributes: [
9 | 'name',
10 | ],
11 | }),
12 | deserializer: new Deserializer({
13 | keyForAttribute: 'camelCase',
14 | }),
15 | },
16 |
17 | posts: {
18 | serializer: new Serializer('posts', {
19 | keyForAttribute: 'camelCase',
20 | attributes: [
21 | 'title',
22 | 'body',
23 | 'category',
24 | 'parts',
25 | ],
26 | category: {
27 | ref: 'id',
28 | included: false,
29 | attributes: ['name'],
30 | },
31 | }),
32 | deserializer: new Deserializer({
33 | keyForAttribute: 'camelCase',
34 | categories: {
35 | valueForRelationship: relationship => ({
36 | id: relationship.id,
37 | name: relationship.name,
38 | }),
39 | },
40 | }),
41 | },
42 |
43 | users: {
44 | serializer: new Serializer('users', {
45 | keyForAttribute: 'camelCase',
46 | attributes: [
47 | 'email',
48 | 'roles',
49 | ],
50 | }),
51 | deserializer: new Deserializer({
52 | keyForAttribute: 'camelCase',
53 | roles: {
54 | valueForRelationship: relationship => ({
55 | id: relationship.id,
56 | }),
57 | },
58 | }),
59 | },
60 | products: {
61 | serializer: new Serializer('products', {
62 | keyForAttribute: 'camelCase',
63 | attributes: [
64 | 'productName',
65 | 'createdAt'
66 | ],
67 | }),
68 | deserializer: new Deserializer({
69 | keyForAttribute: 'camelCase',
70 | }),
71 | },
72 | customers: {
73 | serializer: new Serializer('customers', {
74 | keyForAttribute: 'camelCase',
75 | attributes: [
76 | 'companyName'
77 | ],
78 | }),
79 | deserializer: new Deserializer({
80 | keyForAttribute: 'camelCase'
81 | }),
82 | },
83 | roles: {
84 | serializer: new Serializer('roles', {
85 | keyForAttribute: 'camelCase',
86 | attributes: [
87 | 'name',
88 | ],
89 | }),
90 | deserializer: new Deserializer({
91 | keyForAttribute: 'camelCase',
92 | }),
93 | },
94 |
95 | orders: {
96 | serializer: new Serializer('orders', {
97 | keyForAttribute: 'camelCase',
98 | attributes: [
99 | 'orderDate',
100 | 'requiredDate',
101 | 'shippedDate',
102 | 'shipVia',
103 | 'freight',
104 | 'shipName',
105 | 'shipAddress',
106 | 'shipCity',
107 | 'shipRegion',
108 | 'shipPostalCode',
109 | 'shipCountry',
110 | ],
111 | }),
112 | deserializer: new Deserializer({
113 | keyForAttribute: 'camelCase',
114 | }),
115 | },
116 | };
117 |
118 | export const normalize = (type, data) => {
119 | if (!serializers[type]) {
120 | console.error(`No serializer for ${type}`);
121 | }
122 |
123 | return serializers[type].deserializer.deserialize(data);
124 | };
125 |
126 | export const denormalize = (type, data) => {
127 | const res = serializers[type].serializer.serialize(data);
128 | return data.id ? res : omit(res, 'data.id');
129 | };
130 |
--------------------------------------------------------------------------------
/client/src/api/normalize.spec.js:
--------------------------------------------------------------------------------
1 | import { denormalize, normalize } from './normalize';
2 |
3 | const testNormalize = (type, values) => {
4 | describe(type, () => {
5 | it('denormalize', () => {
6 | expect(denormalize(type, values)).toMatchSnapshot();
7 | });
8 |
9 | it('normalize', async () => (normalize(type, denormalize(type, values)))
10 | .then(res => expect(res).toMatchSnapshot()));
11 | });
12 | };
13 |
14 | describe('normalize', () => {
15 | testNormalize('categories', {
16 | id: 11,
17 | name: 'Category 11',
18 | });
19 |
20 | testNormalize('posts', {
21 | id: 1,
22 | title: 'Title 1',
23 | body: 'Body 1',
24 | category: {
25 | id: 11,
26 | name: 'Category 11',
27 | },
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/client/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Container, Navbar, Nav, NavItem, NavLink, NavDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
4 |
5 | import { getUser, logout } from '../store/auth';
6 |
7 | export class App extends Component {
8 | state = {
9 | isOpen: false,
10 | };
11 |
12 | logout = (e) => {
13 | e.preventDefault();
14 | this.props.logout(this.props.user);
15 | };
16 |
17 | toggle = () => this.setState({
18 | isOpen: !this.state.isOpen,
19 | });
20 |
21 | render() {
22 | const { user } = this.props;
23 | const userIsAdmin = user.roles.includes('admin');
24 |
25 | return (
26 |
27 |
28 |
29 |
52 |
62 |
63 |
64 |
65 | {this.props.children}
66 |
67 |
68 | );
69 | }
70 | }
71 |
72 | export const mapStateToProps = state => ({
73 | user: getUser(state),
74 | });
75 |
76 | export const mapDispatchToProps = dispatch => ({
77 | logout: payload => dispatch(logout('auth', payload)),
78 | });
79 |
80 | export default connect(mapStateToProps, mapDispatchToProps)(App);
81 |
--------------------------------------------------------------------------------
/client/src/components/Auth/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { replace } from 'react-router-redux';
4 | import { SubmissionError } from 'redux-form';
5 |
6 | import { CardSingle } from '../UI';
7 | import LoginForm from './LoginForm';
8 | import { login } from '../../store/auth';
9 |
10 | export class Login extends Component {
11 | componentWillMount() {
12 | localStorage.clear();
13 | }
14 |
15 | onSubmit = values => this.props.login(values)
16 | .then(this.props.redirect)
17 | .catch(({ message }) => {
18 | throw new SubmissionError({ _error: message });
19 | });
20 |
21 | render() {
22 | return (
23 |
24 | Login
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | export const mapStateToProps = state => ({});
32 |
33 | export const mapDispatchToProps = (dispatch, props) => ({
34 | login: payload => dispatch(login('auth', payload)),
35 | redirect: () => dispatch(replace(props.location.query.redirect || '/')),
36 | });
37 |
38 | export default connect(mapStateToProps, mapDispatchToProps)(Login);
39 |
--------------------------------------------------------------------------------
/client/src/components/Auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Alert, Button, Form } from 'reactstrap';
4 |
5 | import { InputField, required } from '../../forms';
6 |
7 | class LoginForm extends Component {
8 | render() {
9 | const { handleSubmit, submitting, error } = this.props;
10 |
11 | return (
12 |
27 | );
28 | }
29 | }
30 |
31 | const validate = (values) => {
32 | const errors = required(values,
33 | 'email',
34 | 'password',
35 | );
36 | return errors;
37 | };
38 |
39 | export default reduxForm({
40 | enableReinitialize: true,
41 | form: 'login',
42 | validate,
43 | })(LoginForm);
44 |
--------------------------------------------------------------------------------
/client/src/components/Auth/index.js:
--------------------------------------------------------------------------------
1 | export Login from './Login';
2 |
--------------------------------------------------------------------------------
/client/src/components/Categories/CategoryForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Button, Form, Col, Row } from 'reactstrap';
4 |
5 | import { InputField, required } from '../../forms';
6 |
7 | class CategoryForm extends Component {
8 | render() {
9 | const { handleSubmit, onSubmit, onDelete, reset, isNew, submitSucceeded } = this.props;
10 |
11 | if (isNew && submitSucceeded) {
12 | setTimeout(() => reset());
13 | }
14 |
15 | const submitOnChange = () => {
16 | if (!isNew) {
17 | setTimeout(() => handleSubmit(onSubmit)(), 0);
18 | }
19 | };
20 |
21 | return (
22 |
40 | );
41 | }
42 | }
43 |
44 | const validate = (values) => {
45 | const errors = required(values, 'name');
46 | return errors;
47 | };
48 |
49 | export default reduxForm({
50 | enableReinitialize: true,
51 | validate,
52 | })(CategoryForm);
53 |
--------------------------------------------------------------------------------
/client/src/components/Categories/CategoryList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { Loading } from '../UI';
4 | import { withResourceList } from '../../hocs';
5 | import CategoryForm from './CategoryForm';
6 |
7 | export class CategoryList extends Component {
8 | componentWillMount() {
9 | this.props.fetchResourceList({ page: { size: 999 } });
10 | }
11 |
12 | render() {
13 | const { resourceList, onSubmit, onDelete } = this.props;
14 |
15 | if (resourceList.empty && resourceList.loading) {
16 | return ();
17 | }
18 |
19 | return (
20 |
21 | {resourceList.data.map(category =>
22 |
23 |
29 |
,
30 | )}
31 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default withResourceList('categories')(CategoryList);
42 |
--------------------------------------------------------------------------------
/client/src/components/Categories/index.js:
--------------------------------------------------------------------------------
1 | export CategoryList from './CategoryList';
2 |
--------------------------------------------------------------------------------
/client/src/components/Customers/CustomerEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { push } from 'react-router-redux';
3 | import { connect } from 'react-redux';
4 |
5 | import { ErrorAlert, Loading, EditHeader } from '../UI';
6 | import { withResource } from '../../hocs';
7 | import CustomerForm from './CustomerForm';
8 | import { getMany, fetchList } from '../../store/api';
9 |
10 | export class CustomerEdit extends Component {
11 | componentWillMount() {
12 | const { params, fetchResource } = this.props;
13 | if (params.id) {
14 | fetchResource({ id: params.id });
15 | }
16 | }
17 |
18 | render() {
19 | const { isNew, error, loading, resource, onSubmit } = this.props;
20 |
21 | if (error) {
22 | return ();
23 | }
24 |
25 | if (loading) {
26 | return ();
27 | }
28 |
29 | return (
30 |
31 | { isNew ? 'New Customer' : resource.company_name }
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export const mapStateToProps = (state, props) => ({
39 | roles: getMany(state)
40 | });
41 |
42 | export const mapDispatchToProps = dispatch => ({
43 | redirectToIndex: () => dispatch(push('/customers'))
44 | });
45 |
46 | export default connect(mapStateToProps, mapDispatchToProps)(
47 | withResource('customers')(CustomerEdit),
48 | );
49 |
--------------------------------------------------------------------------------
/client/src/components/Customers/CustomerForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { isEmpty } from 'lodash';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { Button, Form } from 'reactstrap';
5 |
6 | import { InputField, MultiselectField, required } from '../../forms';
7 |
8 | class CustomerForm extends Component {
9 | render() {
10 | const { handleSubmit, pristine, reset, submitting } = this.props;
11 |
12 | return (
13 |
75 | );
76 | }
77 | }
78 |
79 | const validate = (values) => {
80 | const errors = required(values, 'email');
81 | return errors;
82 | };
83 |
84 | export default reduxForm({
85 | enableReinitialize: true,
86 | form: 'customer',
87 | validate,
88 | })(CustomerForm);
89 |
--------------------------------------------------------------------------------
/client/src/components/Customers/CustomerList.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { find, keyBy } from 'lodash';
4 | import { Button } from 'reactstrap';
5 |
6 | import { ListTable } from '../UI';
7 | import { withResourceList } from '../../hocs';
8 | import CustomerListFilter from './CustomerListFilter';
9 |
10 | const formatDate = date => (new Date(date)).toLocaleString();
11 |
12 | export class CustomerList extends Component {
13 | componentWillMount() {
14 | const { resourceList } = this.props;
15 | this.props.fetchResourceList({ sort: '-companyName', ...resourceList.params });
16 | }
17 |
18 | render() {
19 | const { onFilter } = this.props;
20 | const columns = [
21 | {
22 | attribute: 'companyName',
23 | header: 'Company Name',
24 | rowRender: customer => {customer.companyName},
25 | sortable: true,
26 | },
27 | {
28 | attribute: 'contactName',
29 | header: 'Contact Name',
30 | rowRender: customer => {customer.contactName},
31 | sortable: true,
32 | },
33 | {
34 | attribute: 'createdAt',
35 | header: 'Created At',
36 | rowRender: customer => formatDate(customer.confirmedAt),
37 | sortable: true,
38 | }
39 | ];
40 |
41 | return (
42 |
43 |
44 |
45 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
55 | export const mapStateToProps = state => ({
56 | filter: get(state, 'form.customerListFilter.values') || {}
57 | });
58 |
59 | export default withResourceList('customers')(CustomerList);
60 |
--------------------------------------------------------------------------------
/client/src/components/Customers/CustomerListFilter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { isEmpty } from 'lodash';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { Form, Row, Col } from 'reactstrap';
5 |
6 | import { InputField, SelectField } from '../../forms';
7 |
8 | class CustomerListFilter extends Component {
9 | render() {
10 | const { handleSubmit, onSubmit } = this.props;
11 |
12 | const submitOnChange = () => setTimeout(() => handleSubmit(onSubmit)(), 0);
13 |
14 |
15 | return (
16 |
28 | );
29 | }
30 | }
31 |
32 | export default reduxForm({
33 | form: 'customerListFilter',
34 | destroyOnUnmount: false,
35 | })(CustomerListFilter);
36 |
--------------------------------------------------------------------------------
/client/src/components/Customers/index.js:
--------------------------------------------------------------------------------
1 | export CustomerList from './CustomerList';
2 | export CustomerEdit from './CustomerEdit';
3 |
--------------------------------------------------------------------------------
/client/src/components/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 | import { connect } from 'react-redux';
4 | import { get } from 'lodash';
5 |
6 | import { fetchList, getList, getMap } from '../store/api';
7 |
8 | const formatDate = date => (new Date(date)).toLocaleString();
9 |
10 | export class Dashobard extends Component {
11 | componentWillMount() {
12 | this.props.fetchPostsList();
13 | }
14 |
15 | getCategoryForPost(post) {
16 | const categoryId = String(get(post, 'category.id'));
17 | return this.props.categoriesById[categoryId] || {};
18 | }
19 |
20 | render() {
21 | const { postsList } = this.props;
22 |
23 | return (
24 |
25 |
Latest posts
26 | {postsList.empty && postsList.loading &&
Loading...
}
27 | {postsList.data.map(post =>
28 |
29 | {post.title}
30 | ({this.getCategoryForPost(post).name})
31 | {formatDate(post.createdAt)}
32 |
,
33 | )}
34 |
35 | );
36 | }
37 | }
38 |
39 | export const mapStateToProps = state => ({
40 | postsList: getList(state, 'posts', 'latestPosts'),
41 | categoriesById: getMap(state, 'categories'),
42 | });
43 |
44 | export const mapDispatchToProps = dispatch => ({
45 | fetchPostsList: () => dispatch(fetchList(
46 | 'posts',
47 | { include: 'category', page: { size: 5 }, sort: '-createdAt' },
48 | { list: 'latestPosts' },
49 | )),
50 | });
51 |
52 | export default connect(mapStateToProps, mapDispatchToProps)(Dashobard);
53 |
--------------------------------------------------------------------------------
/client/src/components/Orders/OrderEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { push } from 'react-router-redux';
3 | import { connect } from 'react-redux';
4 | import { get, find, omit } from 'lodash';
5 |
6 | import { ErrorAlert, Loading, EditHeader } from '../UI';
7 | import { withResource } from '../../hocs';
8 | import OrderForm from './OrderForm';
9 | import {
10 | fetchList,
11 | getMany,
12 | } from '../../store/api';
13 |
14 | export class OrderEdit extends Component {
15 | componentWillMount() {
16 | const { params, fetchResource } = this.props;
17 |
18 | if (params.id) {
19 | fetchResource({ id: params.id });
20 | }
21 | }
22 |
23 | render() {
24 | const { isNew, error, loading, resource, onSubmit, orders } = this.props;
25 |
26 | if (error) {
27 | return ();
28 | }
29 |
30 | if (loading) {
31 | return ();
32 | }
33 |
34 | return (
35 |
36 | { isNew ? 'New Order' : resource.title }
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export const mapStateToProps = (state, props) => ({
44 | categories: getMany(state, 'orders'),
45 | });
46 |
47 | export const mapDispatchToProps = dispatch => ({
48 | redirectToIndex: () => dispatch(push('/orders')),
49 | });
50 |
51 | export default connect(mapStateToProps, mapDispatchToProps)(
52 | withResource('orders')(OrderEdit),
53 | );
54 |
--------------------------------------------------------------------------------
/client/src/components/Orders/OrderForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { isEmpty } from 'lodash';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { Button, Form, Col, Row } from 'reactstrap';
5 |
6 | import { InputField, required } from '../../forms';
7 |
8 | class OrderForm extends Component {
9 | render() {
10 | const { handleSubmit, onSubmit, onDelete, reset, isNew, submitSucceeded } = this.props;
11 |
12 | if (isNew && submitSucceeded) {
13 | setTimeout(() => reset());
14 | }
15 |
16 | const submitOnChange = () => {
17 | if (!isNew) {
18 | setTimeout(() => handleSubmit(onSubmit)(), 0);
19 | }
20 | };
21 |
22 | return (
23 |
66 | );
67 | }
68 | }
69 |
70 | export default reduxForm({
71 | enableReinitialize: true,
72 | form: 'order',
73 | })(OrderForm);
74 |
--------------------------------------------------------------------------------
/client/src/components/Orders/OrderList.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { connect } from 'react-redux';
4 | import { get, find, keyBy } from 'lodash';
5 | import { Button } from 'reactstrap';
6 |
7 | import { fetchList, getMap, getMany } from '../../store/api';
8 | import { withResourceList } from '../../hocs';
9 | import { ListHeader, ListTable } from '../UI';
10 |
11 | const formatDate = date => (new Date(date)).toLocaleString();
12 |
13 | export class OrderList extends Component {
14 | componentWillMount() {
15 | const {resourceList} = this.props;
16 | this.props.fetchResourceList({sort: '-orderDate', ...resourceList.params});
17 | }
18 |
19 | render() {
20 | const { resourceList, onFilter, categories } = this.props;
21 | const columns = [
22 | {
23 | attribute: 'order_date',
24 | header: 'Order date',
25 | minWidth: '50px',
26 | rowRender: order => formatDate(order.orderDate),
27 | },
28 | {
29 | attribute: 'shippedDate',
30 | header: 'Shipped date',
31 | rowRender: order => formatDate(order.shippedDate),
32 | sortable: true,
33 | },
34 | {
35 | attribute: 'shipAddress',
36 | header: 'Ship address',
37 | rowRender: order => order.shipAddress,
38 | sortable: true,
39 | },
40 | {
41 | attribute: 'shipCity',
42 | header: 'Ship city',
43 | rowRender: order => order.shipCity,
44 | sortable: true,
45 | },
46 | {
47 | attribute: 'shipRegion',
48 | header: 'Ship region',
49 | rowRender: order => order.shipRegion,
50 | sortable: true,
51 | },
52 | ];
53 |
54 | return (
55 |
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | export const mapStateToProps = state => ({
64 | ordersById: getMap(state, 'orders'),
65 | orders: getMany(state, 'orders'),
66 | });
67 |
68 | export const mapDispatchToProps = dispatch => ({
69 | fetchOrders: () => dispatch(fetchList('orders', { page: { size: 999 } })),
70 | });
71 |
72 | export default connect(mapStateToProps, mapDispatchToProps)(
73 | withResourceList('orders')(OrderList),
74 | );
75 |
--------------------------------------------------------------------------------
/client/src/components/Orders/index.js:
--------------------------------------------------------------------------------
1 | export OrderList from './OrderList';
2 | export OrderEdit from './OrderEdit';
3 |
--------------------------------------------------------------------------------
/client/src/components/Posts/PostEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { push } from 'react-router-redux';
3 | import { connect } from 'react-redux';
4 |
5 | import { ErrorAlert, Loading, EditHeader } from '../UI';
6 | import { withResource } from '../../hocs';
7 | import PostForm from './PostForm';
8 | import {
9 | fetchList,
10 | getMany,
11 | } from '../../store/api';
12 |
13 | export class PostEdit extends Component {
14 | componentWillMount() {
15 | const { params, fetchResource, fetchCategories } = this.props;
16 |
17 | fetchCategories();
18 |
19 | if (params.id) {
20 | fetchResource({ id: params.id, include: 'category' });
21 | }
22 | }
23 |
24 | render() {
25 | const { isNew, error, loading, resource, onSubmit, categories } = this.props;
26 |
27 | if (error) {
28 | return ();
29 | }
30 |
31 | if (loading) {
32 | return ();
33 | }
34 |
35 | return (
36 |
37 |
{ isNew ? 'New Post' : resource.title }
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 | export const mapStateToProps = (state, props) => ({
45 | categories: getMany(state, 'categories'),
46 | });
47 |
48 | export const mapDispatchToProps = dispatch => ({
49 | fetchCategories: () => dispatch(fetchList('categories', { page: { limit: 999 } })),
50 | redirectToIndex: () => dispatch(push('/posts')),
51 | });
52 |
53 | export default connect(mapStateToProps, mapDispatchToProps)(
54 | withResource('posts')(PostEdit),
55 | );
56 |
--------------------------------------------------------------------------------
/client/src/components/Posts/PostForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Field, FieldArray, formValueSelector, reduxForm } from 'redux-form';
4 | import { Button, Form, FormFeedback, Col, Row } from 'reactstrap';
5 |
6 | import { InputField, TextArea, SelectField, required } from '../../forms';
7 |
8 | export const PostFormPart = ({ values, part }) => {
9 | switch (values.type) {
10 | case 'image':
11 | return (
12 |
13 |

14 |
{values.description}
15 |
20 |
25 |
26 | );
27 | case 'text':
28 | return (
29 |
35 | );
36 | default:
37 | return null;
38 | }
39 | };
40 |
41 | export const PostFormParts = ({ parts, fields, meta: { error } }) => (
42 |
43 | {fields.map((part, index) =>
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
,
55 | )}
56 | {error &&
{error}}
57 |
58 |
59 |
60 | );
61 |
62 | export class PostForm extends Component {
63 | render() {
64 | const { parts, categories, handleSubmit, pristine, reset, submitting } = this.props;
65 |
66 | const categoriesOptions = [{
67 | id: '',
68 | name: '-- select category --',
69 | }].concat(categories.map(category => ({
70 | id: category.id,
71 | name: category.name,
72 | })));
73 |
74 | return (
75 |
95 | );
96 | }
97 | }
98 |
99 | const validate = (values) => {
100 | const errors = required(values,
101 | 'title',
102 | 'category.id',
103 | );
104 | return errors;
105 | };
106 |
107 | const selector = formValueSelector('post');
108 |
109 | export const mapStateToProps = state => ({
110 | parts: selector(state, 'parts'),
111 | });
112 |
113 | export default reduxForm({
114 | enableReinitialize: true,
115 | form: 'post',
116 | validate,
117 | })(
118 | connect(mapStateToProps)(PostForm),
119 | );
120 |
--------------------------------------------------------------------------------
/client/src/components/Posts/PostList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 | import { connect } from 'react-redux';
4 | import { get } from 'lodash';
5 | import { Button } from 'reactstrap';
6 |
7 | import { fetchList, getMap, getMany } from '../../store/api';
8 | import { withResourceList } from '../../hocs';
9 | import { ListTable } from '../UI';
10 | import PostListFilter from './PostListFilter';
11 |
12 | const formatDate = date => (new Date(date)).toLocaleString();
13 |
14 | export class PostList extends Component {
15 | componentWillMount() {
16 | const { resourceList } = this.props;
17 | this.props.fetchResourceList({ sort: '-createdAt', ...resourceList.params, include: 'category' });
18 | this.props.fetchCategories();
19 | }
20 |
21 | getCategoryForPost(post) {
22 | const categoryId = String(get(post, 'category.id'));
23 | return this.props.categoriesById[categoryId] || {};
24 | }
25 |
26 | render() {
27 | const { resourceList, onFilter, categories } = this.props;
28 |
29 | const columns = [
30 | {
31 | header: 'Category',
32 | minWidth: '50px',
33 | rowRender: post => this.getCategoryForPost(post).name,
34 | },
35 | {
36 | attribute: 'title',
37 | header: 'Title',
38 | rowRender: post => {post.title},
39 | sortable: true,
40 | },
41 | {
42 | attribute: 'createdAt',
43 | header: 'Created At',
44 | rowRender: post => formatDate(post.createdAt),
45 | sortable: true,
46 | },
47 | ];
48 |
49 | return (
50 |
51 |
52 |
53 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | export const mapStateToProps = state => ({
66 | filter: get(state, 'form.postListFilter.values') || {},
67 | categoriesById: getMap(state, 'categories'),
68 | categories: getMany(state, 'categories'),
69 | });
70 |
71 | export const mapDispatchToProps = dispatch => ({
72 | fetchCategories: () => dispatch(fetchList('categories', { page: { size: 999 } })),
73 | });
74 |
75 | export default connect(mapStateToProps, mapDispatchToProps)(
76 | withResourceList('posts')(PostList),
77 | );
78 |
--------------------------------------------------------------------------------
/client/src/components/Posts/PostListFilter.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Form, Row, Col } from 'reactstrap';
4 |
5 | import { InputField, SelectField } from '../../forms';
6 |
7 | class PostListFilter extends Component {
8 | render() {
9 | const { handleSubmit, onSubmit, categories } = this.props;
10 |
11 | const submitOnChange = () => setTimeout(() => handleSubmit(onSubmit)(), 0);
12 |
13 | const categoriesOptions = [{
14 | id: '',
15 | name: 'All categories',
16 | }].concat(categories.map(category => ({
17 | id: category.id,
18 | name: category.name,
19 | })));
20 |
21 | return (
22 |
43 | );
44 | }
45 | }
46 |
47 | export default reduxForm({
48 | form: 'postListFilter',
49 | destroyOnUnmount: false,
50 | })(PostListFilter);
51 |
--------------------------------------------------------------------------------
/client/src/components/Posts/index.js:
--------------------------------------------------------------------------------
1 | export PostEdit from './PostEdit';
2 | export PostList from './PostList';
3 |
--------------------------------------------------------------------------------
/client/src/components/Products/ProductEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { push } from 'react-router-redux';
3 | import { connect } from 'react-redux';
4 |
5 | import { ErrorAlert, Loading, EditHeader } from '../UI';
6 | import { withResource } from '../../hocs';
7 | import ProductForm from './ProductForm';
8 | import { getMany, fetchList } from '../../store/api';
9 |
10 | export class ProductEdit extends Component {
11 | componentWillMount() {
12 | const { params, fetchResource } = this.props;
13 |
14 | if (params.id) {
15 | fetchResource({ id: params.id });
16 | }
17 | }
18 |
19 | render() {
20 | const { isNew, error, loading, resource, onSubmit } = this.props;
21 |
22 | if (error) {
23 | return ();
24 | }
25 |
26 | if (loading) {
27 | return ();
28 | }
29 |
30 | return (
31 |
32 |
{ isNew ? 'New Product' : resource.productName }
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 |
40 | export const mapDispatchToProps = dispatch => ({
41 | redirectToIndex: () => dispatch(push('/products')),
42 | });
43 |
44 | export default connect(null, mapDispatchToProps)(
45 | withResource('products')(ProductEdit),
46 | );
47 |
--------------------------------------------------------------------------------
/client/src/components/Products/ProductForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { isEmpty } from 'lodash';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { Button, Form } from 'reactstrap';
5 |
6 | import { InputField, MultiselectField, required } from '../../forms';
7 |
8 | class ProductForm extends Component {
9 | render() {
10 | const { handleSubmit, pristine, reset, submitting } = this.props;
11 |
12 | return (
13 |
26 | );
27 | }
28 | }
29 |
30 | const validate = (values) => {
31 | const errors = required(values, 'productName');
32 | return errors;
33 | };
34 |
35 | export default reduxForm({
36 | enableReinitialize: true,
37 | form: 'product',
38 | validate,
39 | })(ProductForm);
40 |
--------------------------------------------------------------------------------
/client/src/components/Products/ProductList.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { find, keyBy } from 'lodash';
4 |
5 | import { ListTable } from '../UI';
6 | import { withResourceList } from '../../hocs';
7 | import { Button } from 'reactstrap';
8 |
9 | const formatDate = date => (new Date(date)).toLocaleString();
10 |
11 | export class ProductList extends Component {
12 | componentWillMount() {
13 | const { resourceList } = this.props;
14 | this.props.fetchResourceList({ sort: '-createdAt', ...resourceList.params });
15 | }
16 |
17 | render() {
18 | const columns = [
19 | {
20 | attribute: 'productName',
21 | header: 'Name',
22 | rowRender: product => {product.productName},
23 | sortable: true,
24 | },
25 | {
26 | attribute: 'createdAt',
27 | header: 'Created At',
28 | rowRender: product => formatDate(product.createdAt),
29 | sortable: true,
30 | }
31 | ];
32 |
33 | return (
34 |
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | export default withResourceList('products')(ProductList);
43 |
--------------------------------------------------------------------------------
/client/src/components/Products/index.js:
--------------------------------------------------------------------------------
1 | export ProductList from './ProductList';
2 | export ProductEdit from './ProductEdit';
3 |
--------------------------------------------------------------------------------
/client/src/components/Routes.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Router, Route, IndexRoute } from 'react-router';
5 | import { UserAuthWrapper } from 'redux-auth-wrapper';
6 |
7 | import { getUser } from '../store/auth';
8 | import App from './App';
9 | import Dashboard from './Dashboard';
10 | import { PostList, PostEdit } from './Posts';
11 | import { CategoryList } from './Categories';
12 | import { UserList, UserEdit } from './Users';
13 | import { CustomerList, CustomerEdit } from './Customers';
14 | import { Login } from './Auth';
15 | import { OrderList, OrderEdit } from './Orders';
16 | import { ProductList, ProductEdit } from './Products';
17 |
18 | const UserIsAuthenticated = UserAuthWrapper({ authSelector: getUser });
19 | const UserIsAdmin = UserAuthWrapper({
20 | authSelector: getUser,
21 | predicate: user => user.roles.includes('admin'),
22 | });
23 |
24 | export class Routes extends PureComponent {
25 | static propTypes = {
26 | history: PropTypes.object,
27 | };
28 |
29 | render() {
30 | const { history } = this.props;
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | export default Routes;
59 |
--------------------------------------------------------------------------------
/client/src/components/UI/CardSingle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Card, CardBlock } from 'reactstrap';
4 |
5 | export default props => (
6 |
7 | {props.children}
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/client/src/components/UI/EditHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Badge } from 'reactstrap';
3 |
4 | export default ({ isNew, children, onDelete }) => (
5 |
6 | { children }
7 | { !isNew && X }
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/client/src/components/UI/ErrorAlert.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from 'reactstrap';
3 |
4 | const ErrorAlert = ({ message }) => {message};
5 |
6 | export default ErrorAlert;
7 |
--------------------------------------------------------------------------------
/client/src/components/UI/ListTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Table } from 'reactstrap';
3 |
4 | import { Loading, Pagination } from './';
5 |
6 | const columnKey = (column, postfix) => `${column.accessor || column.header}-${postfix}`;
7 |
8 | export default (props) => {
9 | const { columns, resourceList, onPageSize, onPageNumber, onSort } = props;
10 | const { sort } = resourceList.params;
11 | const sortedAsc = sort && sort[0] !== '-';
12 |
13 | const sorted = attribute => attribute === sort || `-${attribute}` === sort;
14 |
15 | const toggleSort = attribute => (e) => {
16 | if (attribute === sort) {
17 | onSort(sortedAsc ? `-${attribute}` : attribute);
18 | } else {
19 | onSort(attribute);
20 | }
21 | };
22 |
23 |
24 | if (resourceList.empty && resourceList.loading) {
25 | return ();
26 | }
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | {columns.map(column =>
34 |
39 | {column.header}
40 | {column.sortable && !sorted(column.attribute) && }
41 | {column.sortable && sorted(column.attribute) && !sortedAsc && }
42 | {column.sortable && sorted(column.attribute) && sortedAsc && }
43 | | ,
44 | )}
45 |
46 |
47 |
48 | {resourceList.data.map(item =>
49 |
50 | {columns.map(column =>
51 |
52 | {column.rowRender ? column.rowRender(item) : item[column.attribute]}
53 | | ,
54 | )}
55 |
,
56 | )}
57 |
58 |
59 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/client/src/components/UI/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loading = () => (Loading...
);
4 |
5 | export default Loading;
6 |
--------------------------------------------------------------------------------
/client/src/components/UI/Pagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createUltimatePagination, ITEM_TYPES } from 'react-ultimate-pagination';
3 | import { Input, Label } from 'reactstrap';
4 |
5 | const WrapperComponent = ({ children }) => (
6 |
7 | );
8 |
9 | const withPreventDefault = fn => (event) => {
10 | event.preventDefault();
11 | fn();
12 | };
13 |
14 | const Page = ({ value, isActive, onClick }) => (
15 |
16 | {value}
17 |
18 | );
19 |
20 | const createPageLink = children => ({ onClick }) => (
21 |
22 | {children}
23 |
24 | );
25 |
26 | const itemTypeToComponent = {
27 | [ITEM_TYPES.PAGE]: Page,
28 | [ITEM_TYPES.ELLIPSIS]: createPageLink('...'),
29 | [ITEM_TYPES.FIRST_PAGE_LINK]: createPageLink(«),
30 | [ITEM_TYPES.PREVIOUS_PAGE_LINK]: createPageLink(‹),
31 | [ITEM_TYPES.NEXT_PAGE_LINK]: createPageLink(›),
32 | [ITEM_TYPES.LAST_PAGE_LINK]: createPageLink(»),
33 | };
34 |
35 | const UltimatePaginationBootstrap4 = createUltimatePagination({
36 | itemTypeToComponent,
37 | WrapperComponent,
38 | });
39 |
40 | export default (props) => {
41 | const { resourceList, onPageSize, onPageNumber } = props;
42 | const { page } = resourceList.params || {};
43 | const { size, number } = page || {};
44 | const { total = 1 } = resourceList.meta || {};
45 | const currentPage = number || 1;
46 | const totalPages = Math.ceil(total / (size || 10));
47 |
48 | if (totalPages < 2) {
49 | return null;
50 | }
51 |
52 | return (
53 |
54 |
59 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/client/src/components/UI/index.js:
--------------------------------------------------------------------------------
1 | export CardSingle from './CardSingle';
2 | export EditHeader from './EditHeader';
3 | export ErrorAlert from './ErrorAlert';
4 | export ListTable from './ListTable';
5 | export Loading from './Loading';
6 | export Pagination from './Pagination';
7 |
--------------------------------------------------------------------------------
/client/src/components/Users/UserEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { push } from 'react-router-redux';
3 | import { connect } from 'react-redux';
4 |
5 | import { ErrorAlert, Loading, EditHeader } from '../UI';
6 | import { withResource } from '../../hocs';
7 | import UserForm from './UserForm';
8 | import { getMany, fetchList } from '../../store/api';
9 |
10 | export class UserEdit extends Component {
11 | componentWillMount() {
12 | const { params, fetchResource, fetchRoles } = this.props;
13 |
14 | fetchRoles();
15 |
16 | if (params.id) {
17 | fetchResource({ id: params.id });
18 | }
19 | }
20 |
21 | render() {
22 | const { isNew, error, loading, resource, onSubmit, roles } = this.props;
23 |
24 | if (error) {
25 | return ();
26 | }
27 |
28 | if (loading) {
29 | return ();
30 | }
31 |
32 | return (
33 |
34 | { isNew ? 'New User' : resource.email }
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export const mapStateToProps = (state, props) => ({
42 | roles: getMany(state, 'roles'),
43 | });
44 |
45 | export const mapDispatchToProps = dispatch => ({
46 | fetchRoles: () => dispatch(fetchList('roles', { page: { limit: 999 } })),
47 | redirectToIndex: () => dispatch(push('/users')),
48 | });
49 |
50 | export default connect(mapStateToProps, mapDispatchToProps)(
51 | withResource('users')(UserEdit),
52 | );
53 |
--------------------------------------------------------------------------------
/client/src/components/Users/UserForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Button, Form } from 'reactstrap';
4 |
5 | import { InputField, MultiselectField, required } from '../../forms';
6 |
7 | class UserForm extends Component {
8 | render() {
9 | const { roles, handleSubmit, pristine, reset, submitting } = this.props;
10 |
11 | const rolesOptions = [{
12 | id: '',
13 | name: '-- select role --',
14 | }].concat(roles.map(role => ({
15 | id: role.name,
16 | name: role.name,
17 | })));
18 |
19 | return (
20 |
41 | );
42 | }
43 | }
44 |
45 | const validate = (values) => {
46 | const errors = required(values, 'email');
47 | return errors;
48 | };
49 |
50 | export default reduxForm({
51 | enableReinitialize: true,
52 | form: 'user',
53 | validate,
54 | })(UserForm);
55 |
--------------------------------------------------------------------------------
/client/src/components/Users/UserList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 |
4 | import { ListTable } from '../UI';
5 | import { withResourceList } from '../../hocs';
6 |
7 | const formatDate = date => (new Date(date)).toLocaleString();
8 |
9 | export class UserList extends Component {
10 | componentWillMount() {
11 | const { resourceList } = this.props;
12 | this.props.fetchResourceList({ sort: '-createdAt', ...resourceList.params });
13 | }
14 |
15 | render() {
16 | const columns = [
17 | {
18 | attribute: 'email',
19 | header: 'Email',
20 | rowRender: user => {user.email},
21 | sortable: true,
22 | },
23 | {
24 | attribute: 'createdAt',
25 | header: 'Created At',
26 | rowRender: user => formatDate(user.confirmedAt),
27 | sortable: true,
28 | },
29 | {
30 | attribute: 'role',
31 | header: 'Role',
32 | rowRender: user => user.roles.join(),
33 | },
34 | ];
35 |
36 | return (
37 |
38 | );
39 | }
40 | }
41 |
42 | export default withResourceList('users')(UserList);
43 |
--------------------------------------------------------------------------------
/client/src/components/Users/index.js:
--------------------------------------------------------------------------------
1 | export UserList from './UserList';
2 | export UserEdit from './UserEdit';
3 |
--------------------------------------------------------------------------------
/client/src/forms/fields/InputField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { FormGroup, Label, Input, FormFeedback } from 'reactstrap';
3 |
4 | class InputField extends Component {
5 | static defaultProps = {
6 | type: 'text',
7 | };
8 |
9 | render() {
10 | const { input, type, label, placeholder, meta: { touched, error } } = this.props;
11 | const showError = touched && error;
12 |
13 | return (
14 |
15 | {label && }
16 |
17 | {showError && {error}}
18 |
19 | );
20 | }
21 | }
22 |
23 | export default InputField;
24 |
--------------------------------------------------------------------------------
/client/src/forms/fields/MultiselectField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { FormGroup, Label, Input, FormFeedback } from 'reactstrap';
3 |
4 | class MultiselectField extends Component {
5 | render() {
6 | const { input, label, options, meta: { touched, error } } = this.props;
7 | const showError = touched && error;
8 |
9 | return (
10 |
11 | {label && }
12 |
13 | {options.map(option => (
14 |
15 | ))}
16 |
17 | {showError && {error}}
18 |
19 | );
20 | }
21 | }
22 |
23 | export default MultiselectField;
24 |
--------------------------------------------------------------------------------
/client/src/forms/fields/SelectField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { FormGroup, Label, Input, FormFeedback } from 'reactstrap';
3 |
4 | class SelectField extends Component {
5 | render() {
6 | const { input, label, options, meta: { touched, error } } = this.props;
7 | const showError = touched && error;
8 |
9 | return (
10 |
11 | {label && }
12 |
13 | {options.map(option => (
14 |
15 | ))}
16 |
17 | {showError && {error}}
18 |
19 | );
20 | }
21 | }
22 |
23 | export default SelectField;
24 |
--------------------------------------------------------------------------------
/client/src/forms/fields/TextArea.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { FormGroup, Label, Input, FormFeedback } from 'reactstrap';
3 |
4 | class TextArea extends Component {
5 | static defaultProps = {
6 | type: 'textarea',
7 | };
8 |
9 | render() {
10 | const { input, type, label, rows, placeholder, meta: { touched, error } } = this.props;
11 | const showError = touched && error;
12 |
13 | return (
14 |
15 | {label && }
16 |
17 | {showError && {error}}
18 |
19 | );
20 | }
21 | }
22 |
23 | export default TextArea;
24 |
--------------------------------------------------------------------------------
/client/src/forms/index.js:
--------------------------------------------------------------------------------
1 | export InputField from './fields/InputField';
2 | export TextArea from './fields/TextArea';
3 | export SelectField from './fields/SelectField';
4 | export MultiselectField from './fields/MultiselectField';
5 | export required from './validations/required';
6 |
--------------------------------------------------------------------------------
/client/src/forms/validations/required.js:
--------------------------------------------------------------------------------
1 | import { get, isEmpty, isNumber, set } from 'lodash';
2 |
3 | export default function required(data, ...fields) {
4 | return fields.reduce((errors, field) => {
5 | const value = get(data, field);
6 | if (!isNumber(value) && isEmpty(value)) {
7 | set(errors, field, 'Required');
8 | }
9 | return errors;
10 | }, {});
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/hocs/index.js:
--------------------------------------------------------------------------------
1 | export withResource from './withResource';
2 | export withResourceList from './withResourceList';
3 |
--------------------------------------------------------------------------------
/client/src/hocs/withResource.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { SubmissionError } from 'redux-form';
3 | import compose from 'recompose/compose';
4 | import withHandlers from 'recompose/withHandlers';
5 |
6 | import {
7 | fetchOne,
8 | createResource,
9 | updateResource,
10 | deleteResource,
11 | resourceAction,
12 | getOne,
13 | getError,
14 | } from '../store/api';
15 |
16 | const withResource = (resourceType, resourceMeta = {}) => (WrappedComponent) => {
17 | const enhance = compose(
18 | withHandlers({
19 | onSubmit: props => (values, meta = {}) => {
20 | const { params, createResource, updateResource, redirectToIndex } = props;
21 | const payload = { id: params.id, ...values };
22 | return (params.id ? updateResource : createResource)(payload, meta)
23 | .then(redirectToIndex)
24 | .catch((errors) => { throw new SubmissionError(errors); });
25 | },
26 | onDelete: props => (e) => {
27 | const { deleteResource, resource, redirectToIndex } = props;
28 | e.preventDefault();
29 | deleteResource(resource).then(redirectToIndex);
30 | },
31 | }),
32 | );
33 |
34 | const mapStateToProps = (state, props) => ({
35 | isNew: !props.params.id,
36 | loading: props.params.id && !getOne(state, resourceType, props.params.id),
37 | error: props.params.id && getError(state, resourceType),
38 | resource: getOne(state, resourceType, props.params.id),
39 | });
40 |
41 | const mapDispatchToProps = dispatch => ({
42 | fetchResource: (payload, meta) =>
43 | dispatch(fetchOne(resourceType, payload, { ...resourceMeta, ...meta })),
44 | createResource: (payload, meta) =>
45 | dispatch(createResource(resourceType, payload, { ...resourceMeta, ...meta })),
46 | updateResource: (payload, meta) =>
47 | dispatch(updateResource(resourceType, payload, { ...resourceMeta, ...meta })),
48 | deleteResource: (payload, meta) =>
49 | dispatch(deleteResource(resourceType, payload, { ...resourceMeta, ...meta })),
50 | resourceAction: (payload, meta) =>
51 | dispatch(resourceAction(resourceType, payload, { ...resourceMeta, ...meta })),
52 | });
53 |
54 | return connect(mapStateToProps, mapDispatchToProps)(enhance(WrappedComponent));
55 | };
56 |
57 | export default withResource;
58 |
--------------------------------------------------------------------------------
/client/src/hocs/withResourceList.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { SubmissionError } from 'redux-form';
3 | import { isEmpty, omitBy } from 'lodash';
4 | import compose from 'recompose/compose';
5 | import withHandlers from 'recompose/withHandlers';
6 |
7 | import {
8 | fetchList,
9 | createResource,
10 | updateResource,
11 | deleteResource,
12 | resourceAction,
13 | getList,
14 | } from '../store/api';
15 |
16 | const withResourceList = (resourceType, resourceMeta = {}) => (WrappedComponent) => {
17 | const enhance = compose(
18 | withHandlers({
19 | onFilter: props => (filter) => {
20 | const { resourceList: { params }, fetchResourceList } = props;
21 | const { page: { size } } = params;
22 | const number = 1;
23 | fetchResourceList({ ...params, page: { number, size }, filter: omitBy(filter, isEmpty) });
24 | },
25 | onSort: props => (sort) => {
26 | const { resourceList: { params }, fetchResourceList } = props;
27 | fetchResourceList({ ...params, sort });
28 | },
29 | onPageSize: props => (event) => {
30 | const { resourceList: { params }, fetchResourceList } = props;
31 | const size = event.target.value;
32 | const number = 1;
33 | fetchResourceList({ ...params, page: { number, size } });
34 | },
35 | onPageNumber: props => (number) => {
36 | const { resourceList: { params }, fetchResourceList } = props;
37 | const { page = {} } = params;
38 | fetchResourceList({ ...params, page: { ...page, number } });
39 | },
40 | onSubmit: props => (values, meta = {}) => {
41 | const { createResource, updateResource } = props;
42 | return (values.id ? updateResource : createResource)(values, { list: 'list', ...meta })
43 | .catch((errors) => { throw new SubmissionError(errors); });
44 | },
45 | onDelete: props => resource => (e) => {
46 | const { deleteResource } = props;
47 | e.preventDefault();
48 | deleteResource(resource);
49 | },
50 | }),
51 | );
52 |
53 | const mapStateToProps = (state, props) => ({
54 | resourceList: getList(state, resourceType, resourceMeta.list),
55 | });
56 |
57 | const mapDispatchToProps = dispatch => ({
58 | fetchResourceList: (payload, meta) =>
59 | dispatch(fetchList(resourceType, payload, { ...resourceMeta, ...meta })),
60 | createResource: (payload, meta) =>
61 | dispatch(createResource(resourceType, payload, { ...resourceMeta, ...meta })),
62 | updateResource: (payload, meta) =>
63 | dispatch(updateResource(resourceType, payload, { ...resourceMeta, ...meta })),
64 | deleteResource: (payload, meta) =>
65 | dispatch(deleteResource(resourceType, payload, { ...resourceMeta, ...meta })),
66 | resourceAction: (payload, meta) =>
67 | dispatch(resourceAction(resourceType, payload, { ...resourceMeta, ...meta }))
68 | });
69 |
70 | return connect(mapStateToProps, mapDispatchToProps)(enhance(WrappedComponent));
71 | };
72 |
73 | export default withResourceList;
74 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | .container-main {
2 | padding-top: 15px;
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <% for (var key in htmlWebpackPlugin.files.css) { %>
9 |
10 | <% } %>
11 | React App
12 |
13 |
14 | Loading ...
15 | <% for (var key in htmlWebpackPlugin.files.js) { %>
16 |