├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── rails_redhot_manifest.js │ ├── images │ │ └── rails_redhot │ │ │ └── .keep │ └── stylesheets │ │ └── rails_redhot │ │ └── .keep ├── controllers │ ├── .keep │ └── concerns │ │ └── .keep ├── helpers │ └── .keep ├── jobs │ └── .keep ├── models │ ├── .keep │ └── concerns │ │ └── .keep └── views │ └── .keep ├── bin └── rails ├── config └── routes.rb ├── lib ├── rails_redhot.rb └── rails_redhot │ ├── acts_as_redux.rb │ └── version.rb ├── rails_redhot.gemspec └── test ├── controllers └── .keep ├── dummy ├── Procfile.dev ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.tailwind.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── foobars_controller.rb │ ├── helpers │ │ ├── application_helper.rb │ │ └── foobars_helper.rb │ ├── javascript │ │ └── application.js │ ├── jobs │ │ └── application_job.rb │ ├── models │ │ ├── another_foo_bar.rb │ │ ├── application_record.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── foobar.rb │ └── views │ │ ├── foobars │ │ ├── _editor.html.erb │ │ ├── _foobar.html.erb │ │ ├── _form.html.erb │ │ ├── _notice.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ │ └── layouts │ │ └── application.html.erb ├── bin │ ├── dev │ ├── importmap │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── importmap.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── permissions_policy.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── storage.yml │ └── tailwind.config.js ├── db │ ├── migrate │ │ ├── 20211110151849_create_foobars.rb │ │ └── 20230314093029_create_another_foo_bars.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico ├── test │ ├── fixtures │ │ ├── another_foo_bars.yml │ │ └── foobars.yml │ └── models │ │ ├── another_foo_bar_test.rb │ │ └── foobar_test.rb └── vendor │ └── javascript │ └── .keep ├── fixtures └── files │ └── .keep ├── helpers └── .keep ├── integration ├── .keep └── navigation_test.rb ├── mailers └── .keep ├── models └── .keep ├── rails_redhot_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /log/*.log 4 | /pkg/ 5 | /tmp/ 6 | /test/dummy/db/*.sqlite3 7 | /test/dummy/db/*.sqlite3-* 8 | /test/dummy/log/*.log 9 | /test/dummy/storage/ 10 | /test/dummy/tmp/ 11 | /test/dummy/app/assets/builds/ 12 | /coverage/ 13 | .ruby-version 14 | *.gem 15 | *__ 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 - November 26, 2024 2 | 3 | - Make gem compatible with current Rails 7 and 8 versions 4 | 5 | # 0.2.0 - March 14, 2023 6 | 7 | - Demo application: Use Ruby 3.2.0, Rails 7.0.4.2 8 | - Added simplecov and improve testcoverage 9 | - Added reducer errors (ActiveModel::Errors), separate from the models own error object 10 | - Use deep_dup before dispatching an action to make sure the original action is never 11 | modified by reducer methods. Use deep_symbolize_keys on an action for convenience 12 | - Added after_change callback 13 | 14 | # 0.1.1 - April 14, 2022 15 | 16 | - Fix calling flatten! multiple times erased initial state 17 | - Documentation updates 18 | - Demo application: Use Ruby 3.1.2, Rails 7.0.2 19 | 20 | # 0.1.0 - December 30, 2021 21 | 22 | - Documentation updates 23 | - Use Rails 7.0.0 24 | 25 | # 0.0.2 - December 10, 2021 26 | 27 | - Added testset 28 | - Polished demo application 29 | 30 | # 0.0.1 - November 16, 2021 31 | 32 | - Initial release, not published to rubygems 33 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Specify your gem's dependencies in redhot.gemspec. 5 | gemspec 6 | 7 | group :development do 8 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 9 | gem "importmap-rails", ">= 0.9.4" 10 | 11 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 12 | gem "turbo-rails", ">= 0.9.0" 13 | 14 | # Use Tailwind CSS [https://github.com/rails/tailwindcss-rails] 15 | gem "tailwindcss-rails", ">= 0.5.4" 16 | 17 | gem "sprockets-rails" 18 | end 19 | 20 | # Use sqlite3 as the database for Active Record 21 | gem "sqlite3" 22 | # Use the Puma web server [https://github.com/puma/puma] 23 | gem "puma" 24 | # Use Redis adapter to run Action Cable in production 25 | # gem "redis", "~> 4.0" 26 | # Start debugger with binding.b -- Read more: https://github.com/ruby/debug 27 | # gem "debug", ">= 1.0.0", group: %i[ development test ] 28 | gem "simplecov" 29 | 30 | gem "base64" 31 | gem "bigdecimal" 32 | gem "mutex_m" 33 | gem "drb" 34 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rails_redhot (0.3.0) 5 | rails (>= 7.0.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (8.0.0) 11 | actionpack (= 8.0.0) 12 | activesupport (= 8.0.0) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (8.0.0) 17 | actionpack (= 8.0.0) 18 | activejob (= 8.0.0) 19 | activerecord (= 8.0.0) 20 | activestorage (= 8.0.0) 21 | activesupport (= 8.0.0) 22 | mail (>= 2.8.0) 23 | actionmailer (8.0.0) 24 | actionpack (= 8.0.0) 25 | actionview (= 8.0.0) 26 | activejob (= 8.0.0) 27 | activesupport (= 8.0.0) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (8.0.0) 31 | actionview (= 8.0.0) 32 | activesupport (= 8.0.0) 33 | nokogiri (>= 1.8.5) 34 | rack (>= 2.2.4) 35 | rack-session (>= 1.0.1) 36 | rack-test (>= 0.6.3) 37 | rails-dom-testing (~> 2.2) 38 | rails-html-sanitizer (~> 1.6) 39 | useragent (~> 0.16) 40 | actiontext (8.0.0) 41 | actionpack (= 8.0.0) 42 | activerecord (= 8.0.0) 43 | activestorage (= 8.0.0) 44 | activesupport (= 8.0.0) 45 | globalid (>= 0.6.0) 46 | nokogiri (>= 1.8.5) 47 | actionview (8.0.0) 48 | activesupport (= 8.0.0) 49 | builder (~> 3.1) 50 | erubi (~> 1.11) 51 | rails-dom-testing (~> 2.2) 52 | rails-html-sanitizer (~> 1.6) 53 | activejob (8.0.0) 54 | activesupport (= 8.0.0) 55 | globalid (>= 0.3.6) 56 | activemodel (8.0.0) 57 | activesupport (= 8.0.0) 58 | activerecord (8.0.0) 59 | activemodel (= 8.0.0) 60 | activesupport (= 8.0.0) 61 | timeout (>= 0.4.0) 62 | activestorage (8.0.0) 63 | actionpack (= 8.0.0) 64 | activejob (= 8.0.0) 65 | activerecord (= 8.0.0) 66 | activesupport (= 8.0.0) 67 | marcel (~> 1.0) 68 | activesupport (8.0.0) 69 | base64 70 | benchmark (>= 0.3) 71 | bigdecimal 72 | concurrent-ruby (~> 1.0, >= 1.3.1) 73 | connection_pool (>= 2.2.5) 74 | drb 75 | i18n (>= 1.6, < 2) 76 | logger (>= 1.4.2) 77 | minitest (>= 5.1) 78 | securerandom (>= 0.3) 79 | tzinfo (~> 2.0, >= 2.0.5) 80 | uri (>= 0.13.1) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.8) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.0) 89 | docile (1.4.1) 90 | drb (2.2.1) 91 | erubi (1.13.0) 92 | globalid (1.2.1) 93 | activesupport (>= 6.1) 94 | i18n (1.14.6) 95 | concurrent-ruby (~> 1.0) 96 | importmap-rails (2.0.3) 97 | actionpack (>= 6.0.0) 98 | activesupport (>= 6.0.0) 99 | railties (>= 6.0.0) 100 | io-console (0.7.2) 101 | irb (1.14.1) 102 | rdoc (>= 4.0.0) 103 | reline (>= 0.4.2) 104 | logger (1.6.1) 105 | loofah (2.23.1) 106 | crass (~> 1.0.2) 107 | nokogiri (>= 1.12.0) 108 | mail (2.8.1) 109 | mini_mime (>= 0.1.1) 110 | net-imap 111 | net-pop 112 | net-smtp 113 | marcel (1.0.4) 114 | mini_mime (1.1.5) 115 | minitest (5.25.2) 116 | mutex_m (0.3.0) 117 | net-imap (0.5.1) 118 | date 119 | net-protocol 120 | net-pop (0.1.2) 121 | net-protocol 122 | net-protocol (0.2.2) 123 | timeout 124 | net-smtp (0.5.0) 125 | net-protocol 126 | nio4r (2.7.4) 127 | nokogiri (1.16.7-x86_64-linux) 128 | racc (~> 1.4) 129 | psych (5.2.0) 130 | stringio 131 | puma (6.5.0) 132 | nio4r (~> 2.0) 133 | racc (1.8.1) 134 | rack (3.1.8) 135 | rack-session (2.0.0) 136 | rack (>= 3.0.0) 137 | rack-test (2.1.0) 138 | rack (>= 1.3) 139 | rackup (2.2.1) 140 | rack (>= 3) 141 | rails (8.0.0) 142 | actioncable (= 8.0.0) 143 | actionmailbox (= 8.0.0) 144 | actionmailer (= 8.0.0) 145 | actionpack (= 8.0.0) 146 | actiontext (= 8.0.0) 147 | actionview (= 8.0.0) 148 | activejob (= 8.0.0) 149 | activemodel (= 8.0.0) 150 | activerecord (= 8.0.0) 151 | activestorage (= 8.0.0) 152 | activesupport (= 8.0.0) 153 | bundler (>= 1.15.0) 154 | railties (= 8.0.0) 155 | rails-dom-testing (2.2.0) 156 | activesupport (>= 5.0.0) 157 | minitest 158 | nokogiri (>= 1.6) 159 | rails-html-sanitizer (1.6.0) 160 | loofah (~> 2.21) 161 | nokogiri (~> 1.14) 162 | railties (8.0.0) 163 | actionpack (= 8.0.0) 164 | activesupport (= 8.0.0) 165 | irb (~> 1.13) 166 | rackup (>= 1.0.0) 167 | rake (>= 12.2) 168 | thor (~> 1.0, >= 1.2.2) 169 | zeitwerk (~> 2.6) 170 | rake (13.2.1) 171 | rdoc (6.8.1) 172 | psych (>= 4.0.0) 173 | reline (0.5.11) 174 | io-console (~> 0.5) 175 | securerandom (0.3.2) 176 | simplecov (0.22.0) 177 | docile (~> 1.1) 178 | simplecov-html (~> 0.11) 179 | simplecov_json_formatter (~> 0.1) 180 | simplecov-html (0.13.1) 181 | simplecov_json_formatter (0.1.4) 182 | sprockets (4.2.1) 183 | concurrent-ruby (~> 1.0) 184 | rack (>= 2.2.4, < 4) 185 | sprockets-rails (3.5.2) 186 | actionpack (>= 6.1) 187 | activesupport (>= 6.1) 188 | sprockets (>= 3.0.0) 189 | sqlite3 (2.3.1-x86_64-linux-gnu) 190 | stringio (3.1.2) 191 | tailwindcss-rails (3.0.0) 192 | railties (>= 7.0.0) 193 | tailwindcss-ruby 194 | tailwindcss-ruby (3.4.15-x86_64-linux) 195 | thor (1.3.2) 196 | timeout (0.4.2) 197 | turbo-rails (2.0.11) 198 | actionpack (>= 6.0.0) 199 | railties (>= 6.0.0) 200 | tzinfo (2.0.6) 201 | concurrent-ruby (~> 1.0) 202 | uri (1.0.2) 203 | useragent (0.16.10) 204 | websocket-driver (0.7.6) 205 | websocket-extensions (>= 0.1.0) 206 | websocket-extensions (0.1.5) 207 | zeitwerk (2.7.1) 208 | 209 | PLATFORMS 210 | x86_64-linux 211 | 212 | DEPENDENCIES 213 | base64 214 | bigdecimal 215 | drb 216 | importmap-rails (>= 0.9.4) 217 | mutex_m 218 | puma 219 | rails_redhot! 220 | simplecov 221 | sprockets-rails 222 | sqlite3 223 | tailwindcss-rails (>= 0.5.4) 224 | turbo-rails (>= 0.9.0) 225 | 226 | BUNDLED WITH 227 | 2.5.23 228 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Ivo Herweijer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RailsRedhot gem 2 | ### __REDux pattern for HOTwire == Redhot__ 3 | Single page applications using redux (react) are very popular. 4 | And with good reason, redux makes maintaining the current state of the app easy. 5 | For instance when building some kind of editor, every action of the user is added 6 | to the redux store. All actions can be reduced to the determine the current state of 7 | the editor. Views are rendered using the current state. 8 | Or when building a complex search page for a webshop. Whenever the user selects a category 9 | or price range to filter on this can be an action for the redux store. If the user 10 | hits the back button the last action can be deleted and the current state 11 | regenerated by reducing all remaining actions. The user only sees the last filter 12 | being reverted to what it was before. 13 | 14 | ### What is redux? 15 | ``` 16 | It is a store containing a list of changes. 17 | All changes combined determine the current view state through one or more reducer functions. 18 | The current view state is also stored. 19 | If a new change arrives it only needs to be applied to the current view state. 20 | To undo a change apply all but the last action again to rebuild te view state. 21 | 22 | For example a view contains a number counting the likes for an article. 23 | There are two buttons: increase- and decrease the likes counter. 24 | Clicking on a button adds an action to the store. 25 | The action is passed on to all reducer functions, 26 | each function passes the new computed state on to the next function: 27 | - If the counter value is nil in the current state set it to zero 28 | - If the action was 'increase' then increment the counter value 29 | - If the action was 'decrease' then decrement the counter value 30 | - If the counter value is lower than zero set it to zero 31 | Save the new state in the store. 32 | Render the view showing the updated counter value. 33 | ``` 34 | 35 | ### What are the advantages of redux? 36 | From the [redux website](https://redux.js.org/) (minus the part about plugins): 37 | ``` 38 | Predictable 39 | Redux helps you write applications that behave consistently, 40 | run in different environments (client, server, and native) 41 | and are easy to test. 42 | 43 | Centralized 44 | Centralizing your application's state and logic enables powerful capabilities 45 | like undo/redo, state persistence, and much more. 46 | 47 | Debuggable 48 | The Redux DevTools make it easy to trace when, where, why 49 | and how your application's state changed. 50 | Redux's architecture lets you log changes, use 'time-travel debugging' 51 | and even send complete error reports to a server. 52 | ``` 53 | 54 | ### Remove complexity 55 | Sometimes the actions of the user in the frontend should be sent to a backend 56 | application. For instance when actions of multiple users should be kept in-sync. 57 | In react applications command-query-responsibility-separation (CQRS) is often 58 | used for this purpose. These solutions can become very complex 59 | ([example](https://medium.com/resolvejs/resolve-redux-backend-ebcfc79bbbea), 60 | scroll down a bit for a full architecture picture). 61 | 62 | The Hotwire (Html Over The Wire) approach does an excellent job of removing the need 63 | to build single page apps. Hotwire is the 64 | [default tool](https://world.hey.com/dhh/the-time-is-right-for-hotwire-ecdb9b33) 65 | for frontend development in Rails 7. 66 | However when using hotwire the responsibilty of maintaining frontend state entirely 67 | falls to the backend application. So when building your editor or search page 68 | you need a way to keep track of that state. The redux (also known as flux- or observer-) 69 | pattern is very useful for this purpose. 70 | 71 | ### Hotwire 72 | This gem aims to combine html-over-the-wire approach with the redux pattern to 73 | radically reduce overall complexity of an application. 74 | (At least when compared to for instance react+cqrs application stacks.) 75 | Only four components are required: 76 | 77 | 1. Views, normal rails views rendering the current state and delivered as turbo frames 78 | 2. Actions, just submit buttons that send a request to the backend handled by a controller 79 | 3. Store, keeping the list of actions and current state, managed by this gem and stored 80 | in an activerecord model 81 | 4. Reducers, a set of functions (provided by you) that translate actions to changes in 82 | state. The state can be used again in step 1 83 | 84 | ### Benefits 85 | 86 | - Straightforward workflow 87 | - Common actions (undo, redo, flatten actions to initial state) are provided by this gem. 88 | Combined with turbo frames for rendering partial page updates this makes it easy to 89 | create a very smooth user experience 90 | - You can create a store of attributes within a single ActiveRecord model. 91 | In a Single Page App (SPA) lots of settings may be needed for a good user 92 | experience. It may be a lot of work to store these in multiple models. 93 | A redux store can hold an arbitrary amount of attributes 94 | 95 | ## Usage 96 | ### Model 97 | Create a migration to add a 'text' type attribute to a model that should have a redux store. 98 | In the model add an `acts_as_redux` line, specifying the name of the text attribute. 99 | Add a private method holding all your reducer functions. 100 | See [this example](test/dummy/app/models/foobar.rb). 101 | Note that all reducer functions must return the state object (a Hash). 102 | 103 | ```ruby 104 | class Foobar < ApplicationRecord 105 | include RailsRedhot::ActsAsRedux 106 | 107 | acts_as_redux :my_redux 108 | 109 | private 110 | 111 | def my_redux_reducers 112 | [ 113 | ->(state, action) { 114 | case action[:type] 115 | when :add 116 | state[:total] += 1 117 | when :remove 118 | state[:total] -= 1 119 | end 120 | state 121 | } 122 | ] 123 | end 124 | end 125 | ``` 126 | 127 | Or specify your own reducer method: 128 | 129 | ```ruby 130 | acts_as_redux :my_redux, reducers: :my_list_of_reducers 131 | 132 | def my_list_of_reducers 133 | # ... 134 | ``` 135 | 136 | ### Undo/redo 137 | Every instance of the model now has access to several methods. 138 | For undoing actions there are: `undo?`, `undo_action` and `undo!`, 139 | which you might use in a view like this: 140 | 141 | ```ruby 142 | <%- if foobar.undo? %> 143 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 144 | <%= form.hidden_field :action, value: :undo %> 145 | <%= form.submit "Undo: #{foobar.undo_action['type']}" %> 146 | <% end %> 147 | <% end %> 148 | ``` 149 | In the controller action use the `undo!` method to perform the action. 150 | For redoing actions the similar methods `redo?`, `redo_action` and `redo!` are available. 151 | 152 | ### Flatten 153 | You can 'save' the current state. Essentially this copies the current view state to the initial state 154 | and truncates the list of actions. Redo and undo are not possible until new actions are added. 155 | Methods `flatten?` and `flatten!` can be used in a view and controller: 156 | 157 | ```ruby 158 | <%- if foobar.flatten? %> 159 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 160 | <%= form.hidden_field :action, value: :flatten %> 161 | <%= form.submit "Save changes" %> 162 | <% end %> 163 | <% end %> 164 | ``` 165 | 166 | ### Sequence ID 167 | As a convenience a sequence ID id is available which should always return a unique id 168 | (within the context of the model instance). To get the next sequence id use `next_seq_id`, 169 | to get the current sequence value use `seq_id`. 170 | You could use a sequence in a reducer function to make sure every added item is assigned a unique id. 171 | 172 | ### Dispatch actions and view state 173 | To add an action to the store you can use the `dispatch!` method, passing a hash with the details of the action. 174 | What the content of that hash should be is up to you. 175 | As long as your reducer fuctions can handle the action anything is possible. 176 | 177 | Finally to get the current state the `view_state` method is available. 178 | In a view it can be used like so: 179 | 180 | ```ruby 181 |

182 | There are <%= foobar.view_state['total'] %> items 183 |

184 | 185 | <%- foobar.view_state['items'].each do |item| %> 186 |

187 | <%= CGI.unescape(item['value']) %> 188 |

189 | <% end %> 190 | ``` 191 | 192 | The `view_state` method returns a Hash. What the content of this hash looks like depends 193 | on the reducer functions you have implemented. 194 | 195 | For a full working example see the demo applications [view](test/dummy/app/views/foobars/_editor.html.erb) 196 | and [controller](test/dummy/app/controllers/foobars_controller.rb). 197 | 198 | ### Adding errors 199 | Just like you can add validation errors on a model, you can add errors inside your reducer methods. 200 | There is an ActiveModel::Errors object for redux errors. It is separate from the one for the model and 201 | can be accessed via `reduce_errors`. Use it like this: 202 | 203 | ```ruby 204 | def my_redux_reducers 205 | @my_redux_reducers ||= [ 206 | -> (state, action) { 207 | case action[:type]&.to_sym 208 | when :add 209 | if action[:item].length <= 6 210 | state[:items] << { id: next_seq_id, value: CGI.escape(action[:item]) } 211 | else 212 | reduce_errors.add(:item, :too_long, { count: 6 }) 213 | end 214 | end 215 | 216 | state 217 | } 218 | ] 219 | end 220 | ``` 221 | 222 | Since ActiveModel does not know anything about attributes living inside your redux store 223 | using `reduce_errors.full_messages` won't work. You can create you own error to message translation or 224 | supply `:base` as the attribute name plus a message. 225 | See the [Rails documentation](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add). 226 | 227 | ```ruby 228 | reduce_errors.add(:base, message: 'Item should have between 1 and 6 characters') 229 | ``` 230 | 231 | If any reduce error is present the `dispatch!` method will return false. 232 | To check if there are any errors present (to prevent saving the model) use: 233 | 234 | ```ruby 235 | @foobar.reduce_valid? 236 | ``` 237 | 238 | In the controller you may want to reload the model if the dispatch action gave an error, 239 | so the old state is rendered. 240 | 241 | ### After change callback 242 | Sometimes you want to do something after the redux store has changed. 243 | For instance to manipulate the view state based on all entries 244 | (the reducer methods only handle one action at the time). 245 | This callback method is called after `dispatch!`, `undo!` and `redo!`. 246 | In the model: 247 | 248 | ```ruby 249 | class AnotherFooBar < ApplicationRecord 250 | include RailsRedhot::ActsAsRedux 251 | 252 | acts_as_redux :another_redux_store, after_change: :my_after_change_actions 253 | 254 | private 255 | 256 | def another_redux_store_reducers 257 | [ 258 | -> (state, _action) { 259 | state[:items] ||= [] 260 | state 261 | }, 262 | # ... 263 | ] 264 | end 265 | 266 | def my_after_change_actions 267 | # Do something with the view_state 268 | # view_state[:items].each { do_something } 269 | end 270 | end 271 | ``` 272 | 273 | ## Security 274 | Care must be taken to not introduce any vulnerabilities! 275 | When passing values from the request to the reducer functions treat any string or complex 276 | values as potential candidates for SQL injection. Either sanitize or `CGI.escape` 277 | strings before adding them to the redux store. 278 | 279 | ## Installation 280 | Add this line to your application's Gemfile: 281 | 282 | ```ruby 283 | gem "rails_redhot" 284 | ``` 285 | 286 | And then execute: 287 | ```bash 288 | $ bundle 289 | ``` 290 | 291 | Or install it yourself as: 292 | ```bash 293 | $ gem install rails_redhot 294 | ``` 295 | 296 | ## Demo application 297 | To use the demo application, clone the repo and run rails: 298 | 299 | ```bash 300 | git clone https://github.com/easydatawarehousing/rails_redhot.git 301 | cd rails_redhot 302 | bundle install 303 | cd test/dummy 304 | rails db:setup 305 | bin/dev 306 | ``` 307 | 308 | Then open the [application](http://localhost:3000/foobars). 309 | Click on 'New foobar', 'Add a new foobar' and 'Edit this foobar'. 310 | 311 | ## Test 312 | Run: 313 | 314 | ```bash 315 | rails test test/dummy/test 316 | ``` 317 | 318 | ## License 319 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 320 | 321 | ## Remarks 322 | 323 | - This gem is not designed to handle very large lists of actions and state. 324 | When calling `undo` the state is rebuilt from scratch, 325 | if the list of actions to process is large this would become slow. 326 | One would need add 'savepoints' that regularly save the state and rebuild 327 | the current state from that point forward 328 | - Stricly speaking, hotwire is not needed for this gem to work. Just using 329 | plain old rails views and controllers is fine. Hotwire certainly makes 330 | an application using this gem a lot faster 331 | - No checking on the size of the text attribute used for the store is done 332 | - Currently only one redux store can be added to a model 333 | - Redux store code inspired by: 334 | - https://gist.github.com/eadz/31c87375722397be861a0dbcf7fb7408 335 | - https://github.com/janlelis/redux.rb 336 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) 4 | load "rails/tasks/engine.rake" 5 | 6 | load "rails/tasks/statistics.rake" 7 | 8 | require "bundler/gem_tasks" 9 | require "rake/testtask" 10 | 11 | Rake::TestTask.new(:test) do |t| 12 | t.libs << "test" 13 | t.pattern = "test/**/*_test.rb" 14 | t.verbose = false 15 | end 16 | 17 | task default: :test 18 | -------------------------------------------------------------------------------- /app/assets/config/rails_redhot_manifest.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/assets/config/rails_redhot_manifest.js -------------------------------------------------------------------------------- /app/assets/images/rails_redhot/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/assets/images/rails_redhot/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/rails_redhot/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/assets/stylesheets/rails_redhot/.keep -------------------------------------------------------------------------------- /app/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/controllers/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/helpers/.keep -------------------------------------------------------------------------------- /app/jobs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/jobs/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/models/.keep -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/views/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/app/views/.keep -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path("..", __dir__) 6 | ENGINE_PATH = File.expand_path("../lib/rails_redhot/engine", __dir__) 7 | APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 12 | 13 | require "rails" 14 | # Pick the frameworks you want: 15 | require "active_model/railtie" 16 | require "active_job/railtie" 17 | require "active_record/railtie" 18 | require "active_storage/engine" 19 | require "action_controller/railtie" 20 | # require "action_mailer/railtie" 21 | require "action_view/railtie" 22 | require "action_cable/engine" 23 | require "sprockets/railtie" 24 | require "rails/test_unit/railtie" 25 | require "rails/engine/commands" 26 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | end 3 | -------------------------------------------------------------------------------- /lib/rails_redhot.rb: -------------------------------------------------------------------------------- 1 | require "rails_redhot/version" 2 | require "rails_redhot/acts_as_redux" 3 | 4 | # Module containing all rails_redhot functionality 5 | module RailsRedhot 6 | end 7 | -------------------------------------------------------------------------------- /lib/rails_redhot/acts_as_redux.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsRedhot 4 | # Include ActAsRedux module to add redux functionality to Rails 5 | module ActsAsRedux 6 | extend ActiveSupport::Concern 7 | 8 | class_methods do 9 | def acts_as_redux(store_name, options = {}) 10 | reducers = options.key?(:reducers) ? options[:reducers] : "#{store_name}_reducers".to_sym 11 | reducer_errors = nil 12 | reducer_after_change = options[:after_change] 13 | 14 | store(store_name, accessors: [ :initial_state, :state, :actions, :head, :seq_id ], coder: JSON) 15 | 16 | after_initialize :load_store 17 | 18 | define_method('view_state') do 19 | state || initial_state || {} 20 | end 21 | 22 | define_method('undo?') do 23 | head > -1 24 | end 25 | 26 | define_method('redo?') do 27 | (head + 1) < actions.length 28 | end 29 | 30 | define_method('flatten?') do 31 | undo? 32 | end 33 | 34 | define_method('undo_action') do 35 | undo? ? actions[head] : nil 36 | end 37 | 38 | define_method('redo_action') do 39 | redo? ? actions[head + 1] : nil 40 | end 41 | 42 | define_method('undo!') do 43 | if undo? 44 | self.head -= 1 45 | self.state = initial_state 46 | if head > -1 47 | actions[0..head].each { |action| perform_reduce(action) } 48 | self.send(reducer_after_change) if reducer_after_change 49 | end 50 | true 51 | else 52 | false 53 | end 54 | end 55 | 56 | define_method('redo!') do 57 | if redo? 58 | self.head += 1 59 | perform_reduce(actions[head]) 60 | self.send(reducer_after_change) if reducer_after_change 61 | true 62 | else 63 | false 64 | end 65 | end 66 | 67 | define_method('flatten!') do 68 | self.initial_state = view_state 69 | self.state = nil 70 | self.head = -1 71 | self.actions = [] 72 | true 73 | end 74 | 75 | define_method('reduce_errors') do 76 | reducer_errors 77 | end 78 | 79 | define_method('reduce_valid?') do 80 | reducer_errors.details.empty? 81 | end 82 | 83 | define_method('next_seq_id') do 84 | self.seq_id += 1 85 | end 86 | 87 | define_method('dispatch!') do |action| 88 | # Destroy any redo actions 89 | self.actions.slice!(head + 1, actions.length - head - 1) if redo? 90 | 91 | self.actions << action 92 | self.head += 1 93 | perform_reduce(action.deep_dup.deep_symbolize_keys) 94 | self.send(reducer_after_change) if reducer_after_change 95 | reduce_valid? 96 | end 97 | 98 | # private 99 | 100 | define_method('reset_reduce_errors') do 101 | reducer_errors = ActiveModel::Errors.new(self) 102 | end 103 | 104 | define_method('load_store') do 105 | self.initial_state ||= {} 106 | # self.state is initially nil: no need to store state twice when there are no actions 107 | self.head ||= -1 108 | self.actions ||= [] 109 | self.seq_id ||= 0 110 | 111 | if state.blank? && initial_state.blank? 112 | perform_reduce({}) 113 | else 114 | reset_reduce_errors 115 | end 116 | end 117 | 118 | define_method('all_reducers') do 119 | send(reducers) 120 | end 121 | 122 | define_method('perform_reduce') do |action| 123 | reset_reduce_errors 124 | 125 | self.state = all_reducers.reduce( 126 | view_state.deep_dup.deep_symbolize_keys 127 | ) do |current_state, reducer| 128 | reducer.call(current_state, action) 129 | end 130 | end 131 | 132 | private :load_store, :all_reducers, :perform_reduce, 133 | :initial_state, :state, :actions, :head, 134 | :initial_state=, :state=, :actions=, :head=, :seq_id= 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/rails_redhot/version.rb: -------------------------------------------------------------------------------- 1 | module RailsRedhot 2 | # Gem version 3 | VERSION = "0.3.0" 4 | end 5 | -------------------------------------------------------------------------------- /rails_redhot.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/rails_redhot/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "rails_redhot" 5 | spec.version = RailsRedhot::VERSION 6 | spec.authors = ["Ivo Herweijer"] 7 | spec.email = ["info@edwhs.nl"] 8 | spec.homepage = "https://github.com/easydatawarehousing/rails_redhot" 9 | spec.summary = "REDux pattern for HOTwire == Redhot" 10 | spec.description = "REDux pattern for HOTwire == Redhot" 11 | spec.license = "MIT" 12 | 13 | spec.metadata["allowed_push_host"] = "https://rubygems.org/" 14 | spec.metadata["homepage_uri"] = spec.homepage 15 | spec.metadata["source_code_uri"] = "https://github.com/easydatawarehousing/rails_redhot" 16 | spec.metadata["changelog_uri"] = "https://github.com/easydatawarehousing/rails_redhot/blob/master/CHANGELOG.md" 17 | 18 | spec.files = Dir["{lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "CHANGELOG.md"] 19 | 20 | spec.add_dependency "rails", ">= 7.0.0" 21 | end 22 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/controllers/.keep -------------------------------------------------------------------------------- /test/dummy/Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | css: bin/rails tailwindcss:watch 3 | -------------------------------------------------------------------------------- /test/dummy/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 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../../javascript .js 4 | //= link_tree ../../../vendor/javascript .js 5 | //= link_tree ../builds 6 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/controllers/foobars_controller.rb: -------------------------------------------------------------------------------- 1 | class FoobarsController < ApplicationController 2 | before_action :set_foobar, only: %i[ show edit update update_action destroy ] 3 | 4 | # GET /foobars 5 | def index 6 | @foobars = Foobar.all 7 | end 8 | 9 | # GET /foobars/1 10 | def show 11 | end 12 | 13 | # GET /foobars/new 14 | def new 15 | @foobar = Foobar.new 16 | end 17 | 18 | # GET /foobars/1/edit 19 | def edit 20 | end 21 | 22 | # POST /foobars 23 | def create 24 | @foobar = Foobar.new(foobar_params) 25 | 26 | if @foobar.save 27 | redirect_to @foobar, notice: "Foobar was successfully created" 28 | else 29 | render :new, status: :unprocessable_entity 30 | end 31 | end 32 | 33 | # PATCH/PUT /foobars/1 34 | def update 35 | if @foobar.update(foobar_params) 36 | redirect_to @foobar, notice: "Foobar was successfully updated" 37 | else 38 | render :edit, status: :unprocessable_entity 39 | end 40 | end 41 | 42 | # DELETE /foobars/1 43 | def destroy 44 | @foobar.destroy 45 | redirect_to foobars_url, notice: "Foobar was successfully destroyed" 46 | end 47 | 48 | # PUT /foobars/1/update_action 49 | def update_action 50 | notice = case foobar_update_action_params[:action].to_sym 51 | when :add_foo 52 | do_dispatch(:add, 'Foo') 53 | when :add_bar 54 | do_dispatch(:add, 'Bar') 55 | when :add_custom 56 | do_dispatch(:add, foobar_update_action_params[:custom].strip.capitalize) 57 | when :remove 58 | do_dispatch(:remove, foobar_update_action_params[:action_id]&.to_i) 59 | when :undo 60 | do_undo 61 | when :redo 62 | do_redo 63 | when :flatten 64 | do_flatten 65 | end 66 | 67 | @foobar.reload if !@foobar.reduce_valid? 68 | @foobar.save! if @foobar.changed? 69 | 70 | pp_acts_as_redux 71 | 72 | flash.now.notice = notice if notice.present? 73 | render :edit 74 | end 75 | 76 | private 77 | 78 | def do_dispatch(type, value) 79 | return if value.blank? 80 | 81 | if @foobar.dispatch!(type: type, item: value) 82 | 'Updated !' 83 | else 84 | @foobar.reduce_errors.full_messages.join('. ') 85 | end 86 | end 87 | 88 | def do_undo 89 | @foobar.undo! ? 'Its undone !' : 'Nothing to undo' 90 | end 91 | 92 | def do_redo 93 | @foobar.redo! ? 'Its redone !' : 'Nothing to redo' 94 | end 95 | 96 | def do_flatten 97 | @foobar.flatten! ? 'Saved !' : 'Nothing to save' 98 | end 99 | 100 | def pp_acts_as_redux 101 | puts '='*80 102 | puts "Initial state: #{@foobar.my_redux['initial_state']&.dig('total') || 0} items" 103 | pp @foobar.my_redux['initial_state']&.dig('items') 104 | 105 | puts "\nCurrent state: #{@foobar.my_redux['state']&.dig('total') || 0} items" 106 | pp @foobar.my_redux['state']&.dig('items') 107 | 108 | puts "\nList of actions:" 109 | @foobar.my_redux['actions'].each_with_index do |action, i| 110 | puts "#{i == @foobar.my_redux['head'] ? 'HEAD =>' : ' '*7} #{action['type'].ljust(6)} : '#{action['item']}'" 111 | end 112 | 113 | puts "\nLast used sequence-id: #{@foobar.my_redux['seq_id']}" 114 | puts '='*80 115 | end 116 | 117 | # Use callbacks to share common setup or constraints between actions. 118 | def set_foobar 119 | @foobar = Foobar.find(params[:id]) 120 | end 121 | 122 | # Only allow a list of trusted parameters through. 123 | def foobar_params 124 | params.fetch(:foobar, {}) 125 | end 126 | 127 | # Only allow a list of trusted parameters through. 128 | def foobar_update_action_params 129 | params.require(:foobar).permit(:action, :action_id, :custom) 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/foobars_helper.rb: -------------------------------------------------------------------------------- 1 | module FoobarsHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/models/another_foo_bar.rb: -------------------------------------------------------------------------------- 1 | class AnotherFooBar < ApplicationRecord 2 | 3 | acts_as_redux :another_redux_store, 4 | reducers: :my_redux_reducers, 5 | after_change: :my_after_change_actions 6 | 7 | private 8 | 9 | def my_redux_reducers 10 | [ 11 | # Initialize the store if needed 12 | -> (state, _action) { 13 | state[:items] ||= [] 14 | state 15 | }, 16 | # Add item 17 | -> (state, action) { 18 | state[:items] << { id: next_seq_id, value: CGI.escape(action[:item]), color: '' } 19 | state 20 | } 21 | ] 22 | end 23 | 24 | def my_after_change_actions 25 | if view_state[:items].length.odd? 26 | view_state[:items].each { |item| item[:color] = 'red' } 27 | else 28 | view_state[:items].each { |item| item[:color] = 'green' } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | include RailsRedhot::ActsAsRedux 3 | 4 | primary_abstract_class 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/foobar.rb: -------------------------------------------------------------------------------- 1 | class Foobar < ApplicationRecord 2 | 3 | acts_as_redux :my_redux 4 | 5 | private 6 | 7 | def my_redux_reducers 8 | # Memoize reducers 9 | @my_redux_reducers ||= [ 10 | # Initialize the store if needed 11 | -> (state, _action) { 12 | state[:total] ||= 0 13 | state[:items] ||= [] 14 | 15 | state 16 | }, 17 | # Update total 18 | -> (state, action) { 19 | case action[:type]&.to_sym 20 | when :add 21 | state[:total] += 1 22 | when :remove 23 | state[:total] -= 1 24 | end 25 | 26 | state 27 | }, 28 | # Update items 29 | -> (state, action) { 30 | case action[:type]&.to_sym 31 | when :add 32 | if action[:item].length.between?(1, 6) 33 | state[:items] << { id: next_seq_id, value: CGI.escape(action[:item]) } 34 | else 35 | reduce_errors.add(:base, message: 'Item should have between 1 and 6 characters') 36 | end 37 | when :remove 38 | if action[:item].blank? # Simple check for testing reduce_errors 39 | reduce_errors.add(:base, :blank) 40 | else 41 | state[:items].delete_if do |item| 42 | item.symbolize_keys! 43 | item[:id] == action[:item] 44 | end 45 | end 46 | end 47 | 48 | state 49 | } 50 | ] 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/_editor.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "notice", locals: { notice: notice } %> 2 | 3 |

4 | There are currently <%= foobar.view_state['total'] %> items 5 |

6 | 7 |
8 |

Current items:

9 | 10 | <%- if foobar.view_state.blank? || foobar.view_state['total'].zero? %> 11 |

There are no foobar items yet, please add one by using the buttons below

12 | <% else %> 13 |
14 | <%- foobar.view_state['items'].each do |item| %> 15 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 16 |
17 |

18 | <%= CGI.unescape(item['value']) %> 19 |

20 | <%= form.hidden_field :action, value: :remove %> 21 | <%= form.hidden_field :action_id, value: item['id'] %> 22 | <%= form.submit "X", class: "mr-6 p-3 h-10 text-gray-700 rounded text-sm font-bold bg-gray-200 hover:bg-gray-400" %> 23 | <% end %> 24 |
25 | <% end %> 26 |
27 | <% end %> 28 | 29 |
30 |
31 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 32 | <%= form.hidden_field :action, value: :add_foo %> 33 | <%= form.submit "Add a Foo", class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-500 rounded text-sm font-bold" %> 34 | <% end %> 35 |
36 | 37 |
38 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 39 | <%= form.hidden_field :action, value: :add_bar %> 40 | <%= form.submit "Add a Bar", class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-500 rounded text-sm font-bold" %> 41 | <% end %> 42 |
43 | 44 |
45 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 46 | <%= form.text_field :custom, placeholder: "Add a custom element", class: "w-56" %> 47 | <%= form.hidden_field :action, value: :add_custom %> 48 | <%= form.submit "Add custom", class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-500 rounded text-sm font-bold" %> 49 | <% end %> 50 |
51 | 52 | <%- if foobar.undo? %> 53 |
54 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 55 | <%= form.hidden_field :action, value: :undo %> 56 | <%= form.submit "Undo: #{foobar.undo_action['type'].capitalize}", class: "p-3 h-10 bg-red-700 text-gray-100 hover:bg-red-500 rounded text-sm font-bold" %> 57 | <% end %> 58 |
59 | <% end %> 60 | 61 | <%- if foobar.redo? %> 62 |
63 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 64 | <%= form.hidden_field :action, value: :redo %> 65 | <%= form.submit "Redo: #{foobar.redo_action['type'].capitalize}", class: "p-3 h-10 bg-red-700 text-gray-100 hover:bg-red-500 rounded text-sm font-bold" %> 66 | <% end %> 67 |
68 | <% end %> 69 | 70 | <%- if foobar.flatten? %> 71 |
72 | <%= form_with(model: foobar, url: update_action_foobar_path(foobar), method: :put) do |form| %> 73 | <%= form.hidden_field :action, value: :flatten %> 74 | <%= form.submit "Save changes", class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-500 rounded text-sm font-bold" %> 75 | <% end %> 76 |
77 | <% end %> 78 |
79 |
80 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/_foobar.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%- if foobar.view_state['total'] == 0 %> 4 |

Empty

5 | <% else %> 6 | <%- foobar.view_state['items'].each do |item| %> 7 |

8 | <%= CGI.unescape(item['value']) %> 9 |

10 | <% end %> 11 | <% end %> 12 |
13 | 14 |
15 | <%= link_to "Show foobar #{foobar.id}", foobar, class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold mr-4" %> 16 | <%= link_to "Edit this foobar", edit_foobar_path(foobar), class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold mr-4" %> 17 |
18 |
19 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with(model: foobar) do |form| %> 2 | <% if foobar.errors.any? %> 3 |
4 |

<%= pluralize(foobar.errors.count, "error") %> prohibited this foobar from being saved:

5 | 6 | 11 |
12 | <% end %> 13 | 14 |
15 | <%= form.submit "Add a new Foobar", class: "p-3 h-10 bg-red-700 text-gray-100 hover:bg-red-500 border border-red-700 rounded text-sm font-bold" %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/_notice.html.erb: -------------------------------------------------------------------------------- 1 | <%- if flash.notice %> 2 |

3 | <%= flash.notice %> 4 |

5 | <% end %> 6 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing foobar <%= @foobar.id %>

2 | 3 | 4 | <%= render partial: "editor", locals: { foobar: @foobar } %> 5 | 6 | 7 |
8 | <%= link_to "Show this foobar", @foobar, class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold mr-4" %> 9 | <%= link_to "Back to foobars", foobars_path, class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold" %> 10 |
11 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "notice", locals: { notice: notice } %> 2 | 3 |

Foobars

4 | 5 |
6 | <%= render @foobars %> 7 |
8 | 9 |
10 | <%= link_to "New foobar", new_foobar_path, class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold mr-4" %> 11 |
12 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/new.html.erb: -------------------------------------------------------------------------------- 1 |

New foobar

2 | 3 | <%= render "form", foobar: @foobar %> 4 | 5 |
6 | <%= link_to "Back to foobars", foobars_path, class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold" %> 7 |
8 | -------------------------------------------------------------------------------- /test/dummy/app/views/foobars/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "notice", locals: { notice: notice } %> 2 | 3 | <%= render @foobar %> 4 | 5 |
6 | <%= link_to "Edit this foobar", edit_foobar_path(@foobar), class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold mr-4" %> 7 | <%= link_to "Back to foobars", foobars_path, class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold mr-4" %> 8 | <%= button_to "Destroy this foobar", foobar_path(@foobar), method: :delete, class: "p-3 h-10 bg-green-900 text-gray-100 hover:bg-green-700 border border-green-900 rounded text-sm font-bold" %> 9 |
10 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> 8 | <%= javascript_importmap_tags %> 9 | 10 | 11 | 12 |
13 | <%= yield %> 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /test/dummy/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | clear 3 | 4 | if ! command -v foreman &> /dev/null 5 | then 6 | echo "Installing foreman..." 7 | gem install foreman 8 | fi 9 | 10 | foreman start -f Procfile.dev 11 | -------------------------------------------------------------------------------- /test/dummy/bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "active_storage/engine" 9 | require "action_controller/railtie" 10 | # require "action_mailer/railtie" 11 | require "action_mailbox/engine" 12 | require "action_text/engine" 13 | require "action_view/railtie" 14 | require "action_cable/engine" 15 | require "sprockets/railtie" 16 | require "rails/test_unit/railtie" 17 | 18 | # Require the gems listed in Gemfile, including any gems 19 | # you've limited to :test, :development, or :production. 20 | Bundler.require(*Rails.groups) 21 | require "rails_redhot" 22 | 23 | module Dummy 24 | class Application < Rails::Application 25 | config.load_defaults Rails::VERSION::STRING.to_f 26 | 27 | # For compatibility with applications that use this config 28 | config.action_controller.include_all_helpers = false 29 | 30 | # Configuration for the application, engines, and railties goes here. 31 | # 32 | # These settings can be overridden in specific environments using the files 33 | # in config/environments, which are processed later. 34 | # 35 | # config.time_zone = "Central Time (US & Canada)" 36 | # config.eager_load_paths << Rails.root.join("extras") 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) 3 | 4 | # :nocov: 5 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 6 | # :nocov: 7 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) 8 | -------------------------------------------------------------------------------- /test/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join("tmp/caching-dev.txt").exist? 20 | config.action_controller.perform_caching = true 21 | config.action_controller.enable_fragment_cache_logging = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Store uploaded files on the local file system (see config/storage.yml for options). 34 | config.active_storage.service = :local 35 | 36 | # Print deprecation notices to the Rails logger. 37 | config.active_support.deprecation = :log 38 | 39 | # Raise exceptions for disallowed deprecations. 40 | config.active_support.disallowed_deprecation = :raise 41 | 42 | # Tell Active Support which deprecation messages to disallow. 43 | config.active_support.disallowed_deprecation_warnings = [] 44 | 45 | # Raise an error on page load if there are pending migrations. 46 | config.active_record.migration_error = :page_load 47 | 48 | # Highlight code that triggered database queries in logs. 49 | config.active_record.verbose_query_logs = true 50 | 51 | # Suppress logger output for asset requests. 52 | config.assets.quiet = true 53 | 54 | # Raises error for missing translations. 55 | # config.i18n.raise_on_missing_translations = true 56 | 57 | # Annotate rendered view with file names. 58 | # config.action_view.annotate_rendered_view_with_filenames = true 59 | 60 | # Uncomment if you wish to allow Action Cable access from any origin. 61 | # config.action_cable.disable_request_forgery_protection = true 62 | end 63 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = "http://assets.example.com" 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = "wss://example.com/cable" 46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [ :request_id ] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "dummy_production" 64 | 65 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 66 | # the I18n.default_locale when a translation cannot be found). 67 | config.i18n.fallbacks = true 68 | 69 | # Don't log any deprecations. 70 | config.active_support.report_deprecations = false 71 | 72 | # Use default logging formatter so that PID and timestamp are not suppressed. 73 | config.log_formatter = ::Logger::Formatter.new 74 | 75 | # Use a different logger for distributed setups. 76 | # require "syslog/logger" 77 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 78 | 79 | if ENV["RAILS_LOG_TO_STDOUT"].present? 80 | logger = ActiveSupport::Logger.new(STDOUT) 81 | logger.formatter = config.log_formatter 82 | config.logger = ActiveSupport::TaggedLogging.new(logger) 83 | end 84 | 85 | # Do not dump schema after migrations. 86 | config.active_record.dump_schema_after_migration = false 87 | 88 | # Inserts middleware to perform automatic connection switching. 89 | # The `database_selector` hash is used to pass options to the DatabaseSelector 90 | # middleware. The `delay` is used to determine how long to wait after a write 91 | # to send a subsequent read to the primary. 92 | # 93 | # The `database_resolver` class is used by the middleware to determine which 94 | # database is appropriate to use based on the time delay. 95 | # 96 | # The `database_resolver_context` class is used by the middleware to set 97 | # timestamps for the last write to the primary. The resolver uses the context 98 | # class timestamps to determine how long to wait before reading from the 99 | # replica. 100 | # 101 | # By default Rails will store a last write timestamp in the session. The 102 | # DatabaseSelector middleware is designed as such you can define your own 103 | # strategy for connection switching and pass that into the middleware through 104 | # these configuration options. 105 | # config.active_record.database_selector = { delay: 2.seconds } 106 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 107 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 108 | end 109 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # Turn false under Spring and add config.action_view.cache_template_loading = true 12 | config.cache_classes = true 13 | 14 | # Do not eager load code on boot. This avoids loading your whole application 15 | # just for the purpose of running a single test. If you are using a tool that 16 | # preloads Rails for running tests, you may have to set it to true. 17 | config.eager_load = false 18 | 19 | # Configure public file server for tests with Cache-Control for performance. 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 23 | } 24 | 25 | # Show full error reports and disable caching. 26 | config.consider_all_requests_local = true 27 | config.action_controller.perform_caching = false 28 | config.cache_store = :null_store 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | # Store uploaded files on the local file system in a temporary directory. 37 | config.active_storage.service = :test 38 | 39 | # Print deprecation notices to the stderr. 40 | config.active_support.deprecation = :stderr 41 | 42 | # Raise exceptions for disallowed deprecations. 43 | config.active_support.disallowed_deprecation = :raise 44 | 45 | # Tell Active Support which deprecation messages to disallow. 46 | config.active_support.disallowed_deprecation_warnings = [] 47 | 48 | # Raises error for missing translations. 49 | # config.i18n.raise_on_missing_translations = true 50 | 51 | # Annotate rendered view with file names. 52 | # config.action_view.annotate_rendered_view_with_filenames = true 53 | end 54 | -------------------------------------------------------------------------------- /test/dummy/config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application", preload: true 4 | pin "@hotwired/turbo-rails", to: "turbo.js" 5 | # pin_all_from "app/javascript/controllers", under: "controllers" 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # :nocov: 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 9 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 10 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 11 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | # # Specify URI for violation reports 15 | # # policy.report_uri "/csp-violation-report-endpoint" 16 | # end 17 | 18 | # If you are using UJS then enable automatic nonce generation 19 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 20 | 21 | # Set the nonce only to specific directives 22 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 23 | 24 | # Report CSP violations to a specified URI 25 | # For further information see the following documentation: 26 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 27 | # Rails.application.config.content_security_policy_report_only = true 28 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # :nocov: 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `bin/rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :foobars do 3 | member do 4 | put :update_action 5 | end 6 | end 7 | 8 | root "foobars#index" 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/dummy/config/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | content: [ 5 | './app/helpers/**/*.rb', 6 | './app/javascript/**/*.js', 7 | './app/views/**/*.{erb,html,slim}' 8 | ], 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | sans: ['Inter var', ...defaultTheme.fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [ 17 | require('@tailwindcss/forms'), 18 | require('@tailwindcss/aspect-ratio'), 19 | require('@tailwindcss/typography'), 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20211110151849_create_foobars.rb: -------------------------------------------------------------------------------- 1 | class CreateFoobars < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :foobars do |t| 4 | t.text :my_redux 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20230314093029_create_another_foo_bars.rb: -------------------------------------------------------------------------------- 1 | class CreateAnotherFooBars < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :another_foo_bars do |t| 4 | t.text :another_redux_store 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.0].define(version: 2023_03_14_093029) do 14 | create_table "another_foo_bars", force: :cascade do |t| 15 | t.text "another_redux_store" 16 | t.datetime "created_at", null: false 17 | t.datetime "updated_at", null: false 18 | end 19 | 20 | create_table "foobars", force: :cascade do |t| 21 | t.text "my_redux" 22 | t.datetime "created_at", null: false 23 | t.datetime "updated_at", null: false 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/test/fixtures/another_foo_bars.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | another_foo_bar_new: 3 | another_redux_store: "{\"initial_state\":{},\"head\":1,\"actions\":[{\"item\":\"Foo\"},{\"item\":\"Bar\"},{\"item\":\"Foo\"}],\"seq_id\":4,\"state\":{\"items\":[{\"id\":1,\"value\":\"Foo\",\"color\":\"green\"},{\"id\":2,\"value\":\"Bar\",\"color\":\"green\"}]}}" 4 | -------------------------------------------------------------------------------- /test/dummy/test/fixtures/foobars.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | foobar_new: 3 | my_redux: '' 4 | 5 | foobar_state: 6 | my_redux: "{\"initial_state\":{},\"head\":1,\"actions\":[{\"type\":\"add\",\"item\":\"Foo\"},{\"type\":\"add\",\"item\":\"Bar\"},{\"type\":\"add\",\"item\":\"Custom\"}],\"seq_id\":3,\"state\":{\"total\":2,\"items\":[{\"id\":1,\"value\":\"Foo\"},{\"id\":2,\"value\":\"Bar\"}]}}" 7 | 8 | foobar_initial: 9 | my_redux: "{\"initial_state\":{\"total\":1, \"items\":[{\"id\":1, \"value\":\"Foo\"}]},\"head\":1,\"actions\":[{\"type\":\"add\",\"item\":\"Bar\"},{\"type\":\"add\",\"item\":\"Custom\"}],\"seq_id\":4,\"state\":{\"total\":3,\"items\":[{\"id\":1,\"value\":\"Foo\"},{\"id\":2,\"value\":\"Bar\"},{\"id\":3,\"value\":\"Custom\"}]}}" 10 | -------------------------------------------------------------------------------- /test/dummy/test/models/another_foo_bar_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FooBarTestTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | @another_foobar = another_foo_bars(:another_foo_bar_new) 7 | end 8 | 9 | test "can run after_dispatch method" do 10 | assert @another_foobar.dispatch!({ item: "Bar" }) 11 | assert_equal @another_foobar.view_state[:items].last[:color], 'red' 12 | assert @another_foobar.dispatch!({ item: "Bar" }) 13 | assert_equal @another_foobar.view_state[:items].last[:color], 'green' 14 | end 15 | 16 | test "can run after_dispatch after undo" do 17 | assert @another_foobar.undo! 18 | assert_equal @another_foobar.view_state[:items].last[:color], 'red' 19 | assert @another_foobar.undo! 20 | assert @another_foobar.view_state[:items].blank? 21 | end 22 | 23 | test "can run after_dispatch after redo" do 24 | assert @another_foobar.redo! 25 | assert_equal @another_foobar.view_state[:items].last[:color], 'red' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/dummy/test/models/foobar_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | # Run some test on a new store 4 | class FoobarNewTest < ActiveSupport::TestCase 5 | setup do 6 | @foobar = foobars(:foobar_new) 7 | end 8 | 9 | test "should have a view_state" do 10 | assert @foobar.view_state.present? 11 | assert @foobar.view_state.key?("total") 12 | assert_equal @foobar.view_state["total"], 0 13 | assert @foobar.view_state.key?("items") 14 | assert_equal @foobar.view_state["items"], [] 15 | end 16 | 17 | 18 | test "has nothing to undo" do 19 | assert !@foobar.undo? 20 | end 21 | 22 | test "has no undo action" do 23 | assert_nil @foobar.undo_action 24 | end 25 | 26 | test "can't undo" do 27 | assert !@foobar.undo! 28 | end 29 | 30 | 31 | test "has nothing to redo" do 32 | assert !@foobar.redo? 33 | end 34 | 35 | test "has no redo action" do 36 | assert_nil @foobar.redo_action 37 | end 38 | 39 | test "can't redo" do 40 | assert !@foobar.redo! 41 | end 42 | 43 | 44 | test "has nothing to flatten" do 45 | assert !@foobar.flatten? 46 | end 47 | 48 | test "can flatten" do 49 | assert @foobar.flatten! 50 | end 51 | 52 | 53 | test "can get the next sequence id" do 54 | assert_equal @foobar.seq_id, 0 55 | assert_equal @foobar.next_seq_id, 1 56 | end 57 | 58 | 59 | test "can dispatch an action" do 60 | assert @foobar.dispatch!({ type: "add", item: "Foo" }) 61 | assert @foobar.reduce_valid? 62 | @foobar.save! # Make all keys strings 63 | assert_equal @foobar.my_redux["head"], 0 64 | assert_equal @foobar.my_redux["actions"].length, 1 65 | assert_equal @foobar.my_redux["actions"].first["type"], "add" 66 | assert_equal @foobar.my_redux["actions"].first["item"], "Foo" 67 | assert_equal @foobar.my_redux["seq_id"], 1 68 | end 69 | 70 | 71 | test "can signal a redux error" do 72 | assert !@foobar.dispatch!({ type: "remove", item: "" }) 73 | assert !@foobar.reduce_valid? 74 | assert_equal @foobar.reduce_errors.details.length, 1 75 | assert_equal @foobar.reduce_errors.first.attribute, :base 76 | assert_equal @foobar.reduce_errors.first.type, :blank 77 | end 78 | 79 | test "can signal a redux error with a message" do 80 | assert !@foobar.dispatch!({ type: "add", item: "Too long" }) 81 | assert !@foobar.reduce_valid? 82 | assert_equal @foobar.reduce_errors.full_messages.last, 'Item should have between 1 and 6 characters' 83 | end 84 | end 85 | 86 | # Run some tests on an existing store 87 | class FoobarStateTest < ActiveSupport::TestCase 88 | setup do 89 | @foobar = foobars(:foobar_state) 90 | end 91 | 92 | test "should have a view_state" do 93 | assert @foobar.view_state.present? 94 | assert @foobar.view_state.key?("total") 95 | assert_equal @foobar.view_state["total"], 2 96 | assert @foobar.view_state.key?("items") 97 | assert_equal @foobar.view_state["items"].length, 2 98 | end 99 | 100 | 101 | test "has something to undo" do 102 | assert @foobar.undo? 103 | end 104 | 105 | test "has an undo action" do 106 | assert_equal @foobar.undo_action["type"], "add" 107 | end 108 | 109 | test "can undo" do 110 | assert @foobar.undo! 111 | @foobar.save! # Make all keys strings 112 | assert_equal @foobar.my_redux["head"], 0 113 | assert_equal @foobar.my_redux["actions"].length, 3 114 | end 115 | 116 | 117 | test "has something to redo" do 118 | assert @foobar.redo? 119 | end 120 | 121 | test "has a redo action" do 122 | assert_equal @foobar.redo_action["type"], "add" 123 | end 124 | 125 | test "can redo" do 126 | assert @foobar.redo! 127 | @foobar.save! # Make all keys strings 128 | assert_equal @foobar.my_redux["head"], 2 129 | assert_equal @foobar.my_redux["actions"].length, 3 130 | assert_equal @foobar.my_redux["actions"].last["type"], "add" 131 | assert_equal @foobar.my_redux["actions"].last["item"], "Custom" 132 | assert_equal @foobar.my_redux["seq_id"], 4 133 | end 134 | 135 | 136 | test "has something to flatten" do 137 | assert @foobar.flatten? 138 | end 139 | 140 | test "can flatten" do 141 | assert @foobar.flatten! 142 | @foobar.save! # Make all keys strings 143 | assert_equal @foobar.my_redux["head"], -1 144 | assert_equal @foobar.my_redux["actions"].length, 0 145 | assert_equal @foobar.my_redux["initial_state"]["items"].length, 2 146 | end 147 | 148 | test "can flatten multiple times" do 149 | assert @foobar.flatten! 150 | assert @foobar.flatten! 151 | @foobar.save! # Make all keys strings 152 | assert_equal @foobar.my_redux["initial_state"]["items"].length, 2 153 | end 154 | 155 | test "can get the next sequence id" do 156 | assert_equal @foobar.seq_id, 3 157 | assert_equal @foobar.next_seq_id, 4 158 | end 159 | 160 | 161 | test "can dispatch an add action" do 162 | assert @foobar.dispatch!({ type: "add", item: "Foo" }) 163 | @foobar.save! # Make all keys strings 164 | assert_equal @foobar.my_redux["head"], 2 165 | assert_equal @foobar.my_redux["actions"].length, 3 166 | assert_equal @foobar.my_redux["actions"].last["type"], "add" 167 | assert_equal @foobar.my_redux["actions"].last["item"], "Foo" 168 | assert_equal @foobar.my_redux["seq_id"], 4 169 | 170 | assert_equal @foobar.view_state["total"], 3 171 | assert_equal @foobar.view_state["items"].length, 3 172 | end 173 | 174 | test "can dispatch a remove action" do 175 | assert @foobar.dispatch!({ type: "remove", item: 1 }) 176 | @foobar.save! # Make all keys strings 177 | assert_equal @foobar.my_redux["head"], 2 178 | assert_equal @foobar.my_redux["actions"].length, 3 179 | assert_equal @foobar.my_redux["actions"].last["type"], "remove" 180 | assert_equal @foobar.my_redux["actions"].last["item"], 1 181 | assert_equal @foobar.my_redux["seq_id"], 3 182 | 183 | assert_equal @foobar.view_state["total"], 1 184 | assert_equal @foobar.view_state["items"].length, 1 185 | end 186 | 187 | test "can dispatch an action after redo" do 188 | assert @foobar.redo! 189 | assert @foobar.dispatch!({ type: "add", item: "Foo" }) 190 | @foobar.save! # Make all keys strings 191 | assert_equal @foobar.my_redux["head"], 3 192 | assert_equal @foobar.my_redux["actions"].length, 4 193 | assert_equal @foobar.my_redux["actions"].last["type"], "add" 194 | assert_equal @foobar.my_redux["actions"].last["item"], "Foo" 195 | assert_equal @foobar.my_redux["seq_id"], 5 196 | end 197 | end 198 | 199 | # Run tests on a store with an initial state 200 | class FoobarInitialTest < ActiveSupport::TestCase 201 | setup do 202 | @foobar = foobars(:foobar_initial) 203 | end 204 | 205 | test "has something to undo" do 206 | assert @foobar.undo? 207 | end 208 | 209 | test "can undo twice and keep initial state" do 210 | assert @foobar.undo! 211 | assert_equal @foobar.view_state["total"], 2 212 | assert_equal @foobar.view_state["items"].length, 2 213 | 214 | assert @foobar.undo! 215 | assert_equal @foobar.view_state["total"], 1 216 | assert_equal @foobar.view_state["items"].length, 1 217 | 218 | assert !@foobar.undo! 219 | assert_equal @foobar.view_state["total"], 1 220 | assert_equal @foobar.view_state["items"].length, 1 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /test/dummy/vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/dummy/vendor/javascript/.keep -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/integration/.keep -------------------------------------------------------------------------------- /test/integration/navigation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class NavigationTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easydatawarehousing/rails_redhot/a8f75a9d721ce5124c12ce3b1870a781aa08a584/test/models/.keep -------------------------------------------------------------------------------- /test/rails_redhot_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RailsRedhotTest < ActiveSupport::TestCase 4 | test "it has a version number" do 5 | assert RailsRedhot::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require 'simplecov' 5 | SimpleCov.start { enable_coverage :branch } 6 | 7 | require_relative "../test/dummy/config/environment" 8 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 9 | require "rails/test_help" 10 | 11 | 12 | # Load fixtures from the engine 13 | if ActiveSupport::TestCase.respond_to?(:fixture_paths=) 14 | fixture_path = File.expand_path("dummy/test/fixtures", __dir__) 15 | ActiveSupport::TestCase.fixture_paths << fixture_path 16 | ActionDispatch::IntegrationTest.fixture_paths << fixture_path 17 | ActiveSupport::TestCase.fixture_paths << fixture_path + "/files" 18 | ActiveSupport::TestCase.fixtures :all 19 | end 20 | --------------------------------------------------------------------------------