diff --git a/Gemfile b/Gemfile index 31fa1edcf1..316873f9bd 100644 --- a/Gemfile +++ b/Gemfile @@ -97,7 +97,8 @@ gem 'redis', '>= 4.0', require: ['redis', 'redis/connection/hiredis'] gem 'sidekiq' gem 'sidekiq-scheduler' -gem "cable_ready", "5.0.0.pre3" +gem "cable_ready", "5.0.0.pre9" +gem "stimulus_reflex", "3.5.0.pre9" gem 'combine_pdf' gem 'wicked_pdf' diff --git a/Gemfile.lock b/Gemfile.lock index 8ea9e69ef0..185dd0e34f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -185,8 +185,13 @@ GEM bullet (7.0.3) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - cable_ready (5.0.0.pre3) - rails (>= 5.2) + cable_ready (5.0.0.pre9) + actioncable (>= 5.2) + actionpack (>= 5.2) + actionview (>= 5.2) + activerecord (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) thread-local (>= 1.1.0) cancancan (1.15.0) capybara (3.37.1) @@ -627,6 +632,16 @@ GEM state_machines-activerecord (0.8.0) activerecord (>= 5.1) state_machines-activemodel (>= 0.8.0) + stimulus_reflex (3.5.0.pre9) + actioncable (>= 5.2) + actionpack (>= 5.2) + actionview (>= 5.2) + activesupport (>= 5.2) + cable_ready (>= 5.0.0.pre9) + nokogiri + rack + railties (>= 5.2) + redis stringex (2.8.5) stripe (7.1.0) temple (0.8.2) @@ -708,7 +723,7 @@ DEPENDENCIES bootsnap bugsnag bullet - cable_ready (= 5.0.0.pre3) + cable_ready (= 5.0.0.pre9) cancancan (~> 1.15.0) capybara catalog! @@ -802,6 +817,7 @@ DEPENDENCIES spring spring-commands-rspec state_machines-activerecord + stimulus_reflex (= 3.5.0.pre9) stringex (~> 2.8.5) stripe test-prof diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000000..9aec230539 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000000..d060b74b8a --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :session_id, :current_user + + def connect + # initializes Warden after a cold start, in case you're visitor #1 + env["warden"].authenticated? + # the problem with only using session is that users often login on multiple devices + self.current_user = env["warden"].user + # the problem with only using user is that sometimes you might want to use SR before you login + self.session_id = request.session.id + # and so, we recommend using both + + # this assumes that you want to enable SR for unauthenticated users + reject_unauthorized_connection unless current_user || session_id + + # if you want to disable SR for unauthenticated users, + # comment out the line above and uncomment the line below + # reject_unauthorized_connection unless current_user + end + end +end diff --git a/app/reflexes/application_reflex.rb b/app/reflexes/application_reflex.rb new file mode 100644 index 0000000000..387bee1745 --- /dev/null +++ b/app/reflexes/application_reflex.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ApplicationReflex < StimulusReflex::Reflex + # Put application-wide Reflex behavior and callbacks in this file. + # + # Learn more at: https://docs.stimulusreflex.com/rtfm/reflex-classes + # + # If your ActionCable connection is: `identified_by :current_user` + # delegate :current_user, to: :connection + # + # If you need to localize your Reflexes, you can set the I18n locale here: + # + # before_reflex do + # I18n.locale = :fr + # end + # + # For code examples, considerations and caveats, see: + # https://docs.stimulusreflex.com/rtfm/patterns#internationalization +end diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index e6f94dee84..2c6ceac20f 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -33,6 +33,8 @@ = csrf_meta_tags %meta{name: "turbo-cache-control", content: "no-cache"} + = action_cable_meta_tag + %body{ class: body_classes, "body-scroll": "true", "data-turbo": "false" } / [if lte IE 8] = render partial: "shared/ie_warning" diff --git a/app/views/spree/admin/shared/_head.html.haml b/app/views/spree/admin/shared/_head.html.haml index d4c5856fc3..bd4989ef11 100644 --- a/app/views/spree/admin/shared/_head.html.haml +++ b/app/views/spree/admin/shared/_head.html.haml @@ -1,6 +1,7 @@ %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"} = csrf_meta_tags += action_cable_meta_tag %title - if content_for? :html_title diff --git a/app/webpacker/channels/consumer.js b/app/webpacker/channels/consumer.js new file mode 100644 index 0000000000..b6a99de101 --- /dev/null +++ b/app/webpacker/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. + +import { createConsumer } from "@rails/actioncable"; + +export default createConsumer(); diff --git a/app/webpacker/channels/index.js b/app/webpacker/channels/index.js new file mode 100644 index 0000000000..fa2c1bcc3d --- /dev/null +++ b/app/webpacker/channels/index.js @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context(".", true, /_channel\.js$/); +channels.keys().forEach(channels); diff --git a/app/webpacker/controllers/application_controller.js b/app/webpacker/controllers/application_controller.js new file mode 100644 index 0000000000..462456ff01 --- /dev/null +++ b/app/webpacker/controllers/application_controller.js @@ -0,0 +1,65 @@ +import { Controller } from "@hotwired/stimulus"; +import StimulusReflex from "stimulus_reflex"; + +/* This is your ApplicationController. + * All StimulusReflex controllers should inherit from this class. + * + * Example: + * + * import ApplicationController from './application_controller' + * + * export default class extends ApplicationController { ... } + * + * Learn more at: https://docs.stimulusreflex.com + */ +export default class extends Controller { + connect() { + StimulusReflex.register(this); + } + + /* Application-wide lifecycle methods + * + * Use these methods to handle lifecycle concerns for the entire application. + * Using the lifecycle is optional, so feel free to delete these stubs if you don't need them. + * + * Arguments: + * + * element - the element that triggered the reflex + * may be different than the Stimulus controller's this.element + * + * reflex - the name of the reflex e.g. "Example#demo" + * + * error/noop - the error message (for reflexError), otherwise null + * + * reflexId - a UUID4 or developer-provided unique identifier for each Reflex + */ + + beforeReflex(element, reflex, noop, reflexId) { + // document.body.classList.add('wait') + } + + reflexSuccess(element, reflex, noop, reflexId) { + // show success message + } + + reflexError(element, reflex, error, reflexId) { + // show error message + } + + reflexForbidden(element, reflex, noop, reflexId) { + // Reflex action did not have permission to run + // window.location = '/' + } + + reflexHalted(element, reflex, noop, reflexId) { + // handle aborted Reflex action + } + + afterReflex(element, reflex, noop, reflexId) { + // document.body.classList.remove('wait') + } + + finalizeReflex(element, reflex, noop, reflexId) { + // all operations have completed, animation etc is now safe + } +} diff --git a/app/webpacker/controllers/example_controller.js b/app/webpacker/controllers/example_controller.js index 03477d5a9a..3a1ab11fab 100644 --- a/app/webpacker/controllers/example_controller.js +++ b/app/webpacker/controllers/example_controller.js @@ -1,18 +1,73 @@ -// This is what a basic Stimulus Controller looks like. To apply it to an element you can do: -// div{"data-controller": "example"} -// or: -// div{data: {controller: "example"}} +import ApplicationController from "./application_controller"; -import { Controller } from "stimulus"; +/* This is the custom StimulusReflex controller for the Example Reflex. + * Learn more at: https://docs.stimulusreflex.com + */ +export default class extends ApplicationController { + /* + * Regular Stimulus lifecycle methods + * Learn more at: https://stimulusjs.org/reference/lifecycle-callbacks + * + * If you intend to use this controller as a regular stimulus controller as well, + * make sure any Stimulus lifecycle methods overridden in ApplicationController call super. + * + * Important: + * By default, StimulusReflex overrides the -connect- method so make sure you + * call super if you intend to do anything else when this controller connects. + */ -export default class extends Controller { - // connect() is a built-in lifecycle callback for Stimulus Controllers. It fires when the - // element is loaded on the page, and that also *includes* when some HTML is asynchronously - // injected into the DOM. This means initialization is not tied to the page load event, but - // will also happen dynamically if and when new DOM elements are added or removed. connect() { - console.log("We're connected!"); + super.connect(); + // add your code here, if applicable } -} -// For more info take a look at https://stimulus.hotwired.dev/handbook/introduction + /* Reflex specific lifecycle methods. + * + * For every method defined in your Reflex class, a matching set of lifecycle methods become available + * in this javascript controller. These are optional, so feel free to delete these stubs if you don't + * need them. + * + * Important: + * Make sure to add data-controller="example" to your markup alongside + * data-reflex="Example#dance" for the lifecycle methods to fire properly. + * + * Example: + * + * Dance! + * + * Arguments: + * + * element - the element that triggered the reflex + * may be different than the Stimulus controller's this.element + * + * reflex - the name of the reflex e.g. "Example#dance" + * + * error/noop - the error message (for reflexError), otherwise null + * + * reflexId - a UUID4 or developer-provided unique identifier for each Reflex + */ + + // Assuming you create a "Example#dance" action in your Reflex class + // you'll be able to use the following lifecycle methods: + + // beforeDance(element, reflex, noop, reflexId) { + // element.innerText = 'Putting dance shoes on...' + // } + + // danceSuccess(element, reflex, noop, reflexId) { + // element.innerText = '\nDanced like no one was watching! Was someone watching?' + // } + + // danceError(element, reflex, error, reflexId) { + // console.error('danceError', error); + // element.innerText = "\nCouldn\'t dance!" + // } + + // afterDance(element, reflex, noop, reflexId) { + // element.innerText = '\nWhatever that was, it\'s over now.' + // } + + // finalizeDance(element, reflex, noop, reflexId) { + // element.innerText = '\nNow, the cleanup can begin!' + // } +} diff --git a/app/webpacker/packs/admin.js b/app/webpacker/packs/admin.js index 3f1bed9332..02d879c4f1 100644 --- a/app/webpacker/packs/admin.js +++ b/app/webpacker/packs/admin.js @@ -4,3 +4,12 @@ import { definitionsFromContext } from "stimulus/webpack-helpers"; const application = Application.start(); const context = require.context("controllers", true, /.js$/); application.load(definitionsFromContext(context)); + +import StimulusReflex from "stimulus_reflex"; +import consumer from "../channels/consumer"; +import controller from "../controllers/application_controller"; + +application.consumer = consumer; +StimulusReflex.initialize(application, { controller, isolate: true }); +StimulusReflex.debug = process.env.RAILS_ENV === "development"; +CableReady.initialize({ consumer }); diff --git a/app/webpacker/packs/application.js b/app/webpacker/packs/application.js index 957ea95d98..ff8a4a56ae 100644 --- a/app/webpacker/packs/application.js +++ b/app/webpacker/packs/application.js @@ -22,3 +22,12 @@ mrujs.start({ require.context("../fonts", true); const images = require.context("../images", true); const imagePath = (name) => images(name, true); + +import StimulusReflex from "stimulus_reflex"; +import consumer from "../channels/consumer"; +import controller from "../controllers/application_controller"; + +application.consumer = consumer; +StimulusReflex.initialize(application, { controller, isolate: true }); +StimulusReflex.debug = process.env.RAILS_ENV === "development"; +CableReady.initialize({ consumer }); diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..26ba3b994f --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,19 @@ +development: + adapter: redis + url: <%= ENV.fetch("OFN_REDIS_URL", "redis://localhost:6379/1") %> + channel_prefix: your_application_development + +production: + adapter: redis + url: <%= ENV.fetch("OFN_REDIS_URL", "redis://localhost:6380/0") %> + channel_prefix: your_application_production + +staging: + adapter: redis + url: <%= ENV.fetch("OFN_REDIS_URL", "redis://localhost:6380/0") %> + channel_prefix: your_application_staging + +test: + adapter: redis + url: <%= ENV.fetch("OFN_REDIS_URL", "redis://localhost:6379/1") %> + channel_prefix: your_application_test diff --git a/config/environments/development.rb b/config/environments/development.rb index d7628cceb2..3d75500f51 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -10,6 +10,10 @@ Openfoodnetwork::Application.configure do # since you don't have to restart the web server when you make code changes. config.cache_classes = !!ENV["PROFILE"] + config.action_controller.default_url_options = {host: "localhost", port: 3000} + + config.session_store :cache_store, key: "_sessions_development", compress: true, pool_size: 5, expire_after: 1.year + # :file_store is used by default when no cache store is specifically configured. if !!ENV["PROFILE"] || !!ENV["DEV_CACHING"] config.cache_store = :redis_cache_store, { diff --git a/config/initializers/cable_ready.rb b/config/initializers/cable_ready.rb new file mode 100644 index 0000000000..0375cba039 --- /dev/null +++ b/config/initializers/cable_ready.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +CableReady.configure do |config| + # Enable/disable exiting / warning when the sanity checks fail options: + # `:exit` or `:warn` or `:ignore` + + # config.on_failed_sanity_checks = :exit + + # Enable/disable exiting / warning when there's a new CableReady release + # `:exit` or `:warn` or `:ignore` + + # config.on_new_version_available = :ignore + + # Define your own custom operations + # https://cableready.stimulusreflex.com/customization#custom-operations + + # config.add_operation_name :jazz_hands +end diff --git a/config/initializers/stimulus_reflex.rb b/config/initializers/stimulus_reflex.rb new file mode 100644 index 0000000000..4e21afdc55 --- /dev/null +++ b/config/initializers/stimulus_reflex.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +StimulusReflex.configure do |config| + + config.on_failed_sanity_checks = :warn + + # Enable/disable exiting / warning when the sanity checks fail options: + # `:exit` or `:warn` or `:ignore` + + # config.on_failed_sanity_checks = :exit + + # Override the parent class that the StimulusReflex ActionCable channel inherits from + + # config.parent_channel = "ApplicationCable::Channel" + + # Customize server-side Reflex logging format, with optional colorization: + # Available tokens: session_id, session_id_full, reflex_info, operation, reflex_id, reflex_id_full, mode, selector, operation_counter, connection_id, connection_id_full, timestamp + # Available colors: red, green, yellow, blue, magenta, cyan, white + # You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models + # eg. if your connection is `identified_by :current_user` and your User model has an email attribute, you can access r.email (it will display `-` if the user isn't logged in) + # Learn more at: https://docs.stimulusreflex.com/troubleshooting#stimulusreflex-logging + + # config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" } + + # Optimized for speed, StimulusReflex doesn't enable Rack middleware by default. + # If you are using Page Morphs and your app uses Rack middleware to rewrite part of the request path, you must enable those middleware modules in StimulusReflex. + # + # Learn more about registering Rack middleware in Rails here: https://guides.rubyonrails.org/rails_on_rack.html#configuring-middleware-stack + + # config.middleware.use FirstRackMiddleware + # config.middleware.use SecondRackMiddleware +end diff --git a/package.json b/package.json index 22478f9dc0..b33500daa3 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "@hotwired/turbo": "^7.1.0", "@rails/webpacker": "5.4.3", "babel-loader": "^8.2.3", - "cable_ready": "5.0.0-pre3", + "cable_ready": "5.0.0-pre9", + "stimulus_reflex": "3.5.0-pre9", "flatpickr": "^4.6.9", "foundation-sites": "^5.5.2", "jquery-ui": "1.13.0", diff --git a/yarn.lock b/yarn.lock index 3586aa4cac..336f79bda2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1726,9 +1726,9 @@ resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd" integrity sha512-wa/zupVG0eWxRYJjC1IiPBdt3Lruv0RqGN+/DTMmUWUyMAEB27KXmVY6a8YpUVTM7QwVuaLNGW4EqDgrS2upXQ== -"@hotwired/stimulus@^3.1.0": +"@hotwired/stimulus@>= 3.0", "@hotwired/stimulus@^3.1.0": version "3.1.0" - resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.1.0.tgz#20215251e5afe6e0a3787285181ba1bfc9097df0" + resolved "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.1.0.tgz#20215251e5afe6e0a3787285181ba1bfc9097df0" integrity sha512-iDMHUhiEJ1xFeicyHcZQQgBzhtk5mPR0QZO3L6wtqzMsJEk2TKECuCQTGKjm+KJTHVY0dKq1dOOAWvODjpd2Mg== "@hotwired/turbo@^7.1.0": @@ -2133,6 +2133,11 @@ resolved "https://registry.yarnpkg.com/@orchidjs/sifter/-/sifter-0.9.2.tgz#86c84589fe42c46bb1f5ec1ab8f747a4c928a83b" integrity sha512-8eR1aNWPhDt+cqOevGUajriiZbMzs6nGGdErUjSr99vMAxwMBRztnJu72OT8M7emyHTVjBB8BPjj4rJIoXQgKA== +"@rails/actioncable@>= 6.0": + version "7.0.3" + resolved "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.0.3.tgz#71f08e958883af64f6a20489318b5e95d2c6dc5b" + integrity sha512-Iefl21FZD+ck1di6xSHMYzSzRiNJTHV4NrAzCfDfqc/wPz4xncrP8f2/fJ+2jzwKIaDn76UVMsALh7R5OzsF8Q== + "@rails/webpacker@5.4.3": version "5.4.3" resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.4.3.tgz#cfe2d8faffe7db5001bad50a1534408b4f2efb2f" @@ -4211,10 +4216,10 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cable_ready@5.0.0-pre3: - version "5.0.0-pre3" - resolved "https://registry.yarnpkg.com/cable_ready/-/cable_ready-5.0.0-pre3.tgz#d36bb9648aead748dfdf0f46dfb489799733f330" - integrity sha512-jFjTJ/K/AiVIgjKr63qXGzp9xZOt2/2UyjVpkMkSzFyDHlYILOwnK4WofGwBKS6tXwDZMfohbIzW/p+LKbrlgA== +cable_ready@5.0.0-pre9, "cable_ready@>= 5.0.0-pre9": + version "5.0.0-pre9" + resolved "https://registry.npmjs.org/cable_ready/-/cable_ready-5.0.0-pre9.tgz#9c082b796f7b7add7965ce637a8d8717726be5ab" + integrity sha512-m7XggG+LRPFLsbH78G603F3AAYpkv+VfGCouN6RrP20Mgz9H0+xr7v2DN2F1CeJBBAliYPKcAt/48uTGjSrBaA== dependencies: morphdom "^2.6.1" @@ -12676,6 +12681,15 @@ stimulus@^3.0.1: "@hotwired/stimulus" "^3.1.0" "@hotwired/stimulus-webpack-helpers" "^1.0.0" +stimulus_reflex@3.5.0-pre9: + version "3.5.0-pre9" + resolved "https://registry.npmjs.org/stimulus_reflex/-/stimulus_reflex-3.5.0-pre9.tgz#e99eaf11e9e7476df10cd8f5c557d368f54446b3" + integrity sha512-ZhGUuJaKaWhHU5eGnZQ3pgnv0/pJ4dtNABDtMZRAHNm3ngsHY5dpR/H801a1ozD6J5rpadONvhhBcPjPY/x6NQ== + dependencies: + "@hotwired/stimulus" ">= 3.0" + "@rails/actioncable" ">= 6.0" + cable_ready ">= 5.0.0-pre9" + store2@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf"