├── .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 |
13 | {error && {error}} 14 | 19 | 25 | 26 | 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 |
23 | 24 | 25 | 30 | 31 | 32 | { 33 | isNew 34 | ? 35 | : 36 | } 37 | 38 | 39 |
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 |
14 |
15 | 20 | 25 | 30 | 35 | 40 | 45 | 46 | 51 | 52 | 57 | 58 | 63 | 64 | 69 |
70 |
71 | 72 | 73 |
74 |
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 |
17 | 18 | 19 | 25 | 26 | 27 |
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 |
24 | 25 | 26 | 32 | 38 | 44 | 50 | 56 | 57 | 58 | { 59 | isNew 60 | ? 61 | : 62 | } 63 | 64 | 65 |
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 |
76 | 81 | 87 | 92 | 93 | 94 | 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 |
23 | 24 | 25 | 32 | 33 | 34 | 40 | 41 | 42 |
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 |
14 |
15 | 20 |
21 |
22 | 23 | 24 |
25 |
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 | , 44 | )} 45 | 46 | 47 | 48 | {resourceList.data.map(item => 49 | 50 | {columns.map(column => 51 | , 54 | )} 55 | , 56 | )} 57 | 58 |
39 | {column.header}  40 | {column.sortable && !sorted(column.attribute) && } 41 | {column.sortable && sorted(column.attribute) && !sortedAsc && } 42 | {column.sortable && sorted(column.attribute) && sortedAsc && } 43 |
52 | {column.rowRender ? column.rowRender(item) : item[column.attribute]} 53 |
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 |
    {children}
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 |
    21 |
    22 | 27 |
    28 |
    29 | 35 |
    36 |
    37 | 38 | 39 |
    40 |
    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 |