diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d48a34551..95643aa619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ * enhancements * Add support for Ruby 3.1/3.2. + * Add support for Hotwire + Turbo, default in Rails 7+. + * `:turbo_stream` is now treated as a navigational format, so it works like HTML navigation when using Turbo. Note: if you relied on `:turbo_stream` to be treated as a non-navigational format before, you can reconfigure your `navigational_formats` in the Devise initializer file to exclude it. + * Devise requires the latest `responders` version, which allows configuring the status used for validation error responses (`error_status`) and for redirects after POST/PUT/PATCH/DELETE requests (`redirect_status`). For backwards compatibility, Devise keeps `error_status` as `:ok` which returns a `200 OK` response, and `redirect_status` to `:found` which returns a `302 Found` response, but you can configure it to return `422 Unprocessable Entity` and `303 See Other` to match the behavior expected by Hotwire/Turbo: + + ```ruby + # config/initializers/devise.rb + Devise.setup do |config| + # ... + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + # ... + end + ``` + + These configs are already generated by default with new apps, and existing apps may opt-in as described above. Note that these defaults may change in future versions of Devise, to better match the Rails + Hotwire/Turbo defaults across the board. ### 4.8.1 - 2021-12-16 diff --git a/Gemfile b/Gemfile index 0b41fa0e1b..aac13712ca 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,7 @@ gem "rdoc" gem "rails-controller-testing", github: "rails/rails-controller-testing" -gem "responders", "~> 3.0" +gem "responders", github: "heartcombo/responders", branch: "main" group :test do gem "nokogiri", "< 1.13" diff --git a/Gemfile.lock b/Gemfile.lock index 704f50e976..a9a79c79a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/heartcombo/responders.git + revision: fb9f787055a7a842584ce351793b249676290090 + branch: main + specs: + responders (3.0.1) + actionpack (>= 5.2) + railties (>= 5.2) + GIT remote: https://github.com/rails/rails-controller-testing.git revision: 351c0162df0771c0c48e6a5a886c4c2f0a5d1a74 @@ -189,9 +198,6 @@ GEM rake (13.0.6) rdoc (6.5.0) psych (>= 4.0.0) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) rexml (3.2.5) ruby-openid (2.9.2) ruby2_keywords (0.0.5) @@ -231,7 +237,7 @@ DEPENDENCIES rails (~> 7.0.0) rails-controller-testing! rdoc - responders (~> 3.0) + responders! rexml sqlite3 (~> 1.4) timecop diff --git a/README.md b/README.md index 8fa3f64b9d..cd2e74a830 100644 --- a/README.md +++ b/README.md @@ -476,6 +476,25 @@ Please note: You will still need to add `devise_for` in your routes in order to devise_for :users, skip: :all ``` +### Hotwire/Turbo + +Devise integrates with Hotwire/Turbo by treating such requests as navigational, and configuring certain responses for errors and redirects to match the expected behavior. New apps are generated with the following response configuration by default, and existing apps may opt-in by adding the config to their Devise initializers: + +```ruby +Devise.setup do |config| + # ... + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found respectively`, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other +end +``` + +_Note_: the above statuses configuration may become the default for Devise in a future release. + ### I18n Devise uses flash messages with I18n, in conjunction with the flash keys :notice and :alert. To customize your app, you can set up your locale file: diff --git a/app/controllers/devise/confirmations_controller.rb b/app/controllers/devise/confirmations_controller.rb index 3069c6efbf..5e22079ec8 100644 --- a/app/controllers/devise/confirmations_controller.rb +++ b/app/controllers/devise/confirmations_controller.rb @@ -27,6 +27,7 @@ def show set_flash_message!(:notice, :confirmed) respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) } else + # TODO: use `error_status` when the default changes to `:unprocessable_entity`. respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } end end diff --git a/app/controllers/devise/registrations_controller.rb b/app/controllers/devise/registrations_controller.rb index 1b8a969f82..f1292b4d90 100644 --- a/app/controllers/devise/registrations_controller.rb +++ b/app/controllers/devise/registrations_controller.rb @@ -67,7 +67,7 @@ def destroy Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) set_flash_message! :notice, :destroyed yield resource if block_given? - respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) } + respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status } end # GET /resource/cancel diff --git a/app/controllers/devise/sessions_controller.rb b/app/controllers/devise/sessions_controller.rb index 9090b002f3..7c4ee7d4eb 100644 --- a/app/controllers/devise/sessions_controller.rb +++ b/app/controllers/devise/sessions_controller.rb @@ -77,7 +77,7 @@ def respond_to_on_destroy # support returning empty response on GET request respond_to do |format| format.all { head :no_content } - format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name) } + format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status } end end end diff --git a/app/controllers/devise/unlocks_controller.rb b/app/controllers/devise/unlocks_controller.rb index 1d04d62274..b1487760b6 100644 --- a/app/controllers/devise/unlocks_controller.rb +++ b/app/controllers/devise/unlocks_controller.rb @@ -29,6 +29,7 @@ def show set_flash_message! :notice, :unlocked respond_with_navigational(resource){ redirect_to after_unlock_path_for(resource) } else + # TODO: use `error_status` when the default changes to `:unprocessable_entity`. respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } end end diff --git a/app/controllers/devise_controller.rb b/app/controllers/devise_controller.rb index 9911fa0b85..4bf92a6887 100644 --- a/app/controllers/devise_controller.rb +++ b/app/controllers/devise_controller.rb @@ -15,6 +15,7 @@ class DeviseController < Devise.parent_controller.constantize end prepend_before_action :assert_is_devise_resource! + self.responder = Devise.responder respond_to :html if mimes_for_respond_to.empty? # Override prefixes to consider the scoped view. diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 38d95b85a8..2a666e9257 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -38,6 +38,6 @@
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
+Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
<%= link_to "Back", :back %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb index ba7ab88701..cabfe307ef 100644 --- a/app/views/devise/shared/_error_messages.html.erb +++ b/app/views/devise/shared/_error_messages.html.erb @@ -1,5 +1,5 @@ <% if resource.errors.any? %> -Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
+Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
<%= link_to "Back", :back %> diff --git a/test/support/integration.rb b/test/support/integration.rb index 76d297a499..3ed0d85aaf 100644 --- a/test/support/integration.rb +++ b/test/support/integration.rb @@ -61,8 +61,8 @@ def sign_in_as_admin(options = {}, &block) # account Middleware redirects. # def assert_redirected_to(url) - assert_includes [301, 302], @integration_session.status, - "Expected status to be 301 or 302, got #{@integration_session.status}" + assert_includes [301, 302, 303], @integration_session.status, + "Expected status to be 301, 302, or 303, got #{@integration_session.status}" assert_url url, @integration_session.headers["Location"] end