diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 188365eb0..599eae3b8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -79,6 +79,9 @@ jobs:
env:
RAILS_ENV: test
CC_TEST_REPORTER_ID: true
+ # Use Firefox for system tests until Chrome headless works reliably again.
+ # See https://github.com/SeleniumHQ/selenium/issues/15273
+ BROWSER: firefox
run: bundle exec rspec --color --format RSpec::Github::Formatter --format progress
- name: Upload coverage reports to Codecov
diff --git a/Gemfile b/Gemfile
index 014274b8f..3da54f1ad 100644
--- a/Gemfile
+++ b/Gemfile
@@ -46,7 +46,7 @@ gem 'solid_cable'
gem 'solid_queue'
gem 'sprockets-rails'
gem 'terser', require: false
-gem 'turbolinks'
+gem 'turbo-rails'
# Authentication
gem 'devise', '~> 4.9'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1054a4e5f..cd317d223 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -608,9 +608,6 @@ GEM
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
- turbolinks (5.2.1)
- turbolinks-source (~> 5.2)
- turbolinks-source (5.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
@@ -731,7 +728,7 @@ DEPENDENCIES
sprockets-rails
stackprof
terser
- turbolinks
+ turbo-rails
web-console
webmock
@@ -969,8 +966,6 @@ CHECKSUMS
tilt (2.6.0) sha256=263d748466e0d83e510aa1a2e2281eff547937f0ef06be33d3632721e255f76b
timeout (0.4.3) sha256=9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e
turbo-rails (2.0.13) sha256=c40ac0a3ccd57c129925c8ac524a5dfd1e17fad080906e2d32135721a8bba22f
- turbolinks (5.2.1) sha256=5fea5889c4e2a78a5bd9abda3860c565342b50c6e2593697d5558a08e15cce9c
- turbolinks-source (5.2.0) sha256=362a41fa851a22b0f15cf8f944b6c7c5788f645dc1f61ae25478bb25c3bc85d4
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
unicode-display_width (3.1.4) sha256=8caf2af1c0f2f07ec89ef9e18c7d88c2790e217c482bfc78aaa65eadd5415ac1
unicode-emoji (4.0.4) sha256=2c2c4ef7f353e5809497126285a50b23056cc6e61b64433764a35eff6c36532a
diff --git a/app/assets/javascripts/account_links.coffee b/app/assets/javascripts/account_links.coffee
index b8259b589..855770a97 100644
--- a/app/assets/javascripts/account_links.coffee
+++ b/app/assets/javascripts/account_links.coffee
@@ -9,4 +9,4 @@ ready = ->
$row_value.parents('.row').remove()
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 95813dc20..9cbcee2a3 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -10,8 +10,12 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
-//= require turbolinks
//= require nested_form_fields
//
// app/assets
//= require_tree .
+//
+// Finally, we dispatch a custom event to signal that all assets are loaded.
+// This is used by our custom migration for Turbo to trigger the `turbo-migration:load` event
+const sprocketsLoad = new Event('sprockets:load');
+document.dispatchEvent(sprocketsLoad);
diff --git a/app/assets/javascripts/base.js b/app/assets/javascripts/base.js
index c94e7f238..ba08ab328 100644
--- a/app/assets/javascripts/base.js
+++ b/app/assets/javascripts/base.js
@@ -18,7 +18,7 @@ $.fn.scrollTo = function (selector) {
}, ANIMATION_DURATION);
};
-$(document).on('turbolinks:load', function () {
+$(document).on('turbo-migration:load', function () {
// Update all CSRF tokens on the page to reduce InvalidAuthenticityToken errors
// See https://github.com/rails/jquery-ujs/issues/456 for details
$.rails.refreshCSRFTokens();
@@ -34,7 +34,7 @@ $(document).on('turbolinks:load', function () {
// Initialize Sentry
const sentrySettings = $('meta[name="sentry"]')
- // Workaround for Turbolinks: We must not re-initialize the Relay object when visiting another page
+ // Workaround for Turbo: We must not re-initialize the Relay object when visiting another page
if (sentrySettings && sentrySettings.data()['enabled'] && Sentry.getReplay() === undefined) {
Sentry.init({
dsn: sentrySettings.data('dsn'),
@@ -55,9 +55,6 @@ $(document).on('turbolinks:load', function () {
});
}
- // Enable all tooltips
- $('[data-bs-toggle="tooltip"]').tooltip();
-
// Ensure that the tab button will be in active (selected) state for each page that has 'option' query (e.g. groups or messages page)
$('#' + $('#option').val()).addClass('selected');
});
diff --git a/app/assets/javascripts/bootstrap.js b/app/assets/javascripts/bootstrap.js
deleted file mode 100644
index a0cdc7705..000000000
--- a/app/assets/javascripts/bootstrap.js
+++ /dev/null
@@ -1,4 +0,0 @@
-jQuery(function () {
- $("a[rel~=popover], .has-popover").popover();
- $("a[rel~=tooltip], .has-tooltip").tooltip();
-});
diff --git a/app/assets/javascripts/collections.coffee b/app/assets/javascripts/collections.coffee
index 54d3df6a1..41d1bb4c3 100644
--- a/app/assets/javascripts/collections.coffee
+++ b/app/assets/javascripts/collections.coffee
@@ -13,4 +13,4 @@ update_collections_tasks_order = (e) ->
$(input_element).val(index);
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/editor.coffee b/app/assets/javascripts/editor.coffee
index 82eaff307..9332e1ad0 100644
--- a/app/assets/javascripts/editor.coffee
+++ b/app/assets/javascripts/editor.coffee
@@ -6,6 +6,14 @@ initializeAce = ->
$(document).on 'fields_added.nested_form_fields', ->
$ initializeEditors()
+unloadEditors = (event) ->
+ $('.editor').each (_, editor) ->
+ aceEditor = ace.edit(editor)
+ content = aceEditor.getValue();
+ aceEditor.destroy()
+ editor.textContent = content;
+ return
+
initializeEditors = ->
$('.editor').each ->
editor = ace.edit(this)
@@ -17,6 +25,8 @@ initializeEditors = ->
else
editor.getSession().on 'change', (e) ->
hiddenContent.val(editor.getValue()).trigger('change')
+ $(document).one('turbo:visit', unloadEditors);
+ $(window).one('beforeunload', unloadEditors);
setAceEditorValue = (editor, value) ->
aceEditor = $(editor).parent().find('.editor')[0]
@@ -34,4 +44,4 @@ root.getModeByFileExtension = getModeByFileExtension
root.changeEditorMode = changeEditorMode
root.setAceEditorValue = setAceEditorValue
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/flash_message.coffee b/app/assets/javascripts/flash_message.coffee
index 26083d338..d37a5de19 100644
--- a/app/assets/javascripts/flash_message.coffee
+++ b/app/assets/javascripts/flash_message.coffee
@@ -12,4 +12,4 @@ $(document).ajaxComplete (event, request) ->
ready = ->
$('#flash-message').children().first().delay(5000).slideUp 'medium'
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/groups.coffee b/app/assets/javascripts/groups.coffee
index 474994025..3fbdd17b9 100644
--- a/app/assets/javascripts/groups.coffee
+++ b/app/assets/javascripts/groups.coffee
@@ -3,4 +3,4 @@ ready = ->
$(e.currentTarget.parentElement).html(data["button"])
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/labels.coffee b/app/assets/javascripts/labels.coffee
index e5edb9e00..bd6170545 100644
--- a/app/assets/javascripts/labels.coffee
+++ b/app/assets/javascripts/labels.coffee
@@ -97,12 +97,19 @@ ready = ->
I18n.t('labels.javascripts.max_limit_reached', {limit: limit})
$('.labels-select2-tag').on 'select2:select', clear_input
+ $(document).one('turbo:visit', destroy_select2);
return
clear_input = ->
$('.labels-select2-tag').siblings(".select2").find("textarea").val("");
return
+destroy_select2 = ->
+ selectElement = $('.labels-select2-tag');
+ selectElement.select2('destroy');
+ selectElement.off('select2:select');
+ return
+
root = exports ? this;
root.verify_label_name = verify_label_name;
diff --git a/app/assets/javascripts/labels_index.coffee b/app/assets/javascripts/labels_index.coffee
index dc56c7a9f..6f0d58be4 100644
--- a/app/assets/javascripts/labels_index.coffee
+++ b/app/assets/javascripts/labels_index.coffee
@@ -185,4 +185,4 @@ ready = ->
table = new LabelsTable(table_container);
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/markdown_editor.js b/app/assets/javascripts/markdown_editor.js
index ade619d66..bcff19b22 100644
--- a/app/assets/javascripts/markdown_editor.js
+++ b/app/assets/javascripts/markdown_editor.js
@@ -180,7 +180,7 @@ const setResizeBtn = (formInput, editor) => {
});
};
-$(document).on("turbolinks:load", function () {
+$(document).on("turbo-migration:load", function () {
initializeMarkdownEditors();
disableImageUpload();
});
diff --git a/app/assets/javascripts/nbp_wallet.js b/app/assets/javascripts/nbp_wallet.js
index 21830e40e..f8bfc05f1 100644
--- a/app/assets/javascripts/nbp_wallet.js
+++ b/app/assets/javascripts/nbp_wallet.js
@@ -26,17 +26,7 @@ const countdownValidity = () => {
}
};
-window.addEventListener("turbolinks:before-render", () => {
- clearInterval(intervalID);
- clearTimeout(timeoutID);
-});
-
-window.addEventListener("beforeunload", () => {
- clearInterval(intervalID);
- clearTimeout(timeoutID);
-});
-
-$(document).on('turbolinks:load', function () {
+$(document).on('turbo-migration:load', function () {
if (window.location.pathname !== Routes.nbp_wallet_connect_users_path()) {
return;
}
@@ -49,4 +39,14 @@ $(document).on('turbolinks:load', function () {
templateValidity = document.querySelector('[data-id="nbp_wallet_qr_code"]').dataset.remainingValidity - 5;
checkStatus();
intervalID = setInterval(countdownValidity, 1000);
+
+ $(document).one("turbo:visit", () => {
+ clearInterval(intervalID);
+ clearTimeout(timeoutID);
+ });
+
+ $(window).one("beforeunload", () => {
+ clearInterval(intervalID);
+ clearTimeout(timeoutID);
+ });
});
diff --git a/app/assets/javascripts/task_files.coffee b/app/assets/javascripts/task_files.coffee
index 83d391a78..ed07a434b 100644
--- a/app/assets/javascripts/task_files.coffee
+++ b/app/assets/javascripts/task_files.coffee
@@ -68,4 +68,4 @@ initializeOnUpload = ->
if fullPath
$(this).parents('.file-container').find('.file-name').val(fullName)
-$(document).on 'turbolinks:load', ready
+$(document).on 'turbo-migration:load', ready
diff --git a/app/assets/javascripts/tasks_base.coffee b/app/assets/javascripts/tasks_base.coffee
index 28d972911..1dacb7d30 100644
--- a/app/assets/javascripts/tasks_base.coffee
+++ b/app/assets/javascripts/tasks_base.coffee
@@ -32,4 +32,4 @@ loadComments = (url, $wait_icon, $comment_box, onSucess)->
root = exports ? this;
root.loadComments = loadComments
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/tasks_checklist.coffee b/app/assets/javascripts/tasks_checklist.coffee
index 9ceabf9eb..b9766b16e 100644
--- a/app/assets/javascripts/tasks_checklist.coffee
+++ b/app/assets/javascripts/tasks_checklist.coffee
@@ -142,4 +142,4 @@ update_checklist = ->
, 100
-$(document).on('turbolinks:load', initialize_checklist)
+$(document).on('turbo-migration:load', initialize_checklist)
diff --git a/app/assets/javascripts/tasks_export.coffee b/app/assets/javascripts/tasks_export.coffee
index 6a5614206..eefaeaf77 100644
--- a/app/assets/javascripts/tasks_export.coffee
+++ b/app/assets/javascripts/tasks_export.coffee
@@ -55,4 +55,4 @@ exportConfirm = (taskId, accountLinkId, pushType) ->
root = exports ? this;
root.exportTaskStart = exportTaskStart
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/tasks_form.coffee b/app/assets/javascripts/tasks_form.coffee
index cdf6a7d63..8713dcac9 100644
--- a/app/assets/javascripts/tasks_form.coffee
+++ b/app/assets/javascripts/tasks_form.coffee
@@ -16,6 +16,8 @@ initializeLoadSelect2 = ->
closeOnSelect: false
placeholder: I18n.t('tasks.javascripts.select_groups')
+ $(document).one('turbo:visit', destroy_select2);
+
initializeFileTypeSelection = ->
$('body').on 'keyup', '.file-name', (event) ->
editor = $(this).parents('.file-container').find('.editor')[0]
@@ -30,6 +32,10 @@ initializeVisibilityWarning = ->
$('#task_access_level_public').on 'change', ->
warning_message.addClass('d-none')
+destroy_select2 = ->
+ $('#task_programming_language_id').select2('destroy');
+ $('.my-group').select2('destroy');
+ return
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
$(document).on('select2:locales:loaded', initializeLoadSelect2)
diff --git a/app/assets/javascripts/tasks_import.coffee b/app/assets/javascripts/tasks_import.coffee
index 46a266174..43c999a3a 100644
--- a/app/assets/javascripts/tasks_import.coffee
+++ b/app/assets/javascripts/tasks_import.coffee
@@ -61,4 +61,4 @@ importConfirm = (importId, subfileId, importType) ->
})
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/tasks_index.coffee b/app/assets/javascripts/tasks_index.coffee
index a53c4a47a..a6ffabff4 100644
--- a/app/assets/javascripts/tasks_index.coffee
+++ b/app/assets/javascripts/tasks_index.coffee
@@ -1,6 +1,5 @@
ready = ->
initCollapsable($('.description'), '95px')
- window.addEventListener 'resize', -> initCollapsable($('.description'), '95px')
initializeDynamicHideShow()
initializeFilter()
initializeIndexComments()
@@ -25,6 +24,8 @@ initializeSelect2 = ->
closeOnSelect: false
placeholder: I18n.t('tasks.javascripts.all_languages')
+ $(document).one('turbo:visit', destroy_select2);
+
toggleHideShowMore = (element) ->
$parent = $(element).parent()
$toggle = $(element).find '.more-btn'
@@ -138,6 +139,12 @@ initializeInputFieldEnterCallback = ->
$('.search-submit-button-tag').click();
event.preventDefault();
+destroy_select2 = ->
+ $('.defaultSelect2').select2('destroy');
+ $('.language-box').select2('destroy');
+ return
+
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
+$(window).on('resize', -> initCollapsable($('.description'), '95px'))
$(document).on('select2:locales:loaded', initializeSelect2)
diff --git a/app/assets/javascripts/tasks_ratings.coffee b/app/assets/javascripts/tasks_ratings.coffee
index 60d055fa9..cf7638575 100644
--- a/app/assets/javascripts/tasks_ratings.coffee
+++ b/app/assets/javascripts/tasks_ratings.coffee
@@ -96,4 +96,4 @@ initializeRatings = ->
)
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/tasks_show.coffee b/app/assets/javascripts/tasks_show.coffee
index e3296158c..9ee540cd3 100644
--- a/app/assets/javascripts/tasks_show.coffee
+++ b/app/assets/javascripts/tasks_show.coffee
@@ -22,4 +22,4 @@ initializeShowComments = ->
$comment_box.addClass('hide')
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/javascripts/users.coffee b/app/assets/javascripts/users.coffee
index 637d951d1..4c78ac47f 100644
--- a/app/assets/javascripts/users.coffee
+++ b/app/assets/javascripts/users.coffee
@@ -14,4 +14,4 @@ ready = ->
new bootstrap.Modal('#notification-modal').show();
-$(document).on('turbolinks:load', ready)
+$(document).on('turbo-migration:load', ready)
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.scss b/app/assets/stylesheets/bootstrap_and_overrides.css.scss
index 220726975..6090a9130 100644
--- a/app/assets/stylesheets/bootstrap_and_overrides.css.scss
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css.scss
@@ -62,6 +62,23 @@
}
.btn-group {
+
+ // Align buttons in a link group, similar to `.btn-group .btn + .btn` etc.
+ .button_to {
+ &:not(:first-child) {
+ margin-left: -1px;
+ .btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+
+ &:not(:last-child) .btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+
.btn-important {
white-space: normal;
padding: 10px 20px;
diff --git a/app/controllers/account_links_controller.rb b/app/controllers/account_links_controller.rb
index 0c81faed7..d6411b3ae 100644
--- a/app/controllers/account_links_controller.rb
+++ b/app/controllers/account_links_controller.rb
@@ -24,10 +24,12 @@ def create # rubocop:disable Metrics/AbcSize
respond_to do |format|
if @account_link.save
- format.html { redirect_to @user, notice: t('common.notices.object_created', model: AccountLink.model_name.human) }
+ format.html do
+ redirect_to @user, notice: t('common.notices.object_created', model: AccountLink.model_name.human), status: :see_other
+ end
format.json { render :show, status: :created, location: @account_link }
else
- format.html { render :new }
+ format.html { render :new, status: :unprocessable_content }
format.json { render json: @account_link.errors, status: :unprocessable_content }
end
end
@@ -36,10 +38,14 @@ def create # rubocop:disable Metrics/AbcSize
def update
respond_to do |format|
if @account_link.update(account_link_params)
- format.html { redirect_to @account_link.user, notice: t('common.notices.object_updated', model: AccountLink.model_name.human) }
+ format.html do
+ redirect_to @account_link.user,
+ notice: t('common.notices.object_updated', model: AccountLink.model_name.human),
+ status: :see_other
+ end
format.json { render :show, status: :ok, location: @account_link }
else
- format.html { render :edit }
+ format.html { render :edit, status: :unprocessable_content }
format.json { render json: @account_link.errors, status: :unprocessable_content }
end
end
@@ -48,7 +54,9 @@ def update
def destroy
@account_link.destroy
respond_to do |format|
- format.html { redirect_to @account_link.user, notice: t('common.notices.object_deleted', model: AccountLink.model_name.human) }
+ format.html do
+ redirect_to @account_link.user, notice: t('common.notices.object_deleted', model: AccountLink.model_name.human), status: :see_other
+ end
format.json { head :no_content }
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 58d11b5af..eb2a41001 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -79,12 +79,12 @@ def render_error(message, status) # rubocop:disable Metrics/AbcSize
respond_to do |format|
format.any do
if redirect_loop? || unauthorized_nbp_request?(status)
- redirect_to :root, alert: message
+ redirect_to :root, alert: message, status: :see_other
elsif current_user.nil? && status == :unauthorized
store_location_for(:user, request.fullpath) if current_user.nil?
- redirect_to new_user_session_path, alert: t('common.errors.not_signed_in')
+ redirect_to new_user_session_path, alert: t('common.errors.not_signed_in'), status: :see_other
else
- redirect_back fallback_location: :root, allow_other_host: false, alert: message
+ redirect_back fallback_location: :root, allow_other_host: false, alert: message, status: :see_other
end
end
format.json { render json: {error: message}, status: }
diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb
index f895f8e6a..0b93d0d29 100644
--- a/app/controllers/collections_controller.rb
+++ b/app/controllers/collections_controller.rb
@@ -41,34 +41,34 @@ def create
# Redirect back to task#show as collections with tasks can only be created from that view
create_collection_with_task
elsif @collection.save
- redirect_to Collection, notice: t('common.notices.object_created', model: Collection.model_name.human)
+ redirect_to Collection, notice: t('common.notices.object_created', model: Collection.model_name.human), status: :see_other
else
- render :new
+ render :new, status: :unprocessable_content
end
end
def update
if @collection.update(collection_params)
- redirect_to Collection, notice: t('common.notices.object_updated', model: Collection.model_name.human)
+ redirect_to Collection, notice: t('common.notices.object_updated', model: Collection.model_name.human), status: :see_other
else
- render :edit
+ render :edit, status: :unprocessable_content
end
end
def remove_task
redirect_target = params[:return_to_task] ? Task.find(params[:task]) : @collection
if @collection.remove_task(params[:task])
- redirect_to redirect_target, notice: t('common.notices.object_removed', model: Task.model_name.human)
+ redirect_to redirect_target, notice: t('common.notices.object_removed', model: Task.model_name.human), status: :see_other
else
- redirect_to redirect_target, alert: t('.cannot_remove_alert')
+ redirect_to redirect_target, alert: t('.cannot_remove_alert'), status: :see_other
end
end
def remove_all
if @collection.remove_all
- redirect_to @collection, notice: t('.success_notice')
+ redirect_to @collection, notice: t('.success_notice'), status: :see_other
else
- redirect_to @collection, alert: t('.cannot_remove_alert')
+ redirect_to @collection, alert: t('.cannot_remove_alert'), status: :see_other
end
end
@@ -76,12 +76,12 @@ def push_collection
errors = push_tasks
if errors.empty?
- redirect_to @collection, notice: t('.push_external_notice', account_link: @account_link.name)
+ redirect_to @collection, notice: t('.push_external_notice', account_link: @account_link.name), status: :see_other
else
errors.each do |error|
logger.debug(error)
end
- redirect_to @collection, alert: t('.not_working', account_link: @account_link.name)
+ redirect_to @collection, alert: t('.not_working', account_link: @account_link.name), status: :see_other
end
end
@@ -89,14 +89,14 @@ def download_all
binary_zip_data = ProformaService::ExportTasks.call(tasks: @collection.tasks, options: {version: params[:version]})
send_data(binary_zip_data.string, type: 'application/zip', filename: "#{@collection.title}.zip", disposition: 'attachment')
rescue ProformaXML::PostGenerateValidationError => e
- redirect_to :root, danger: JSON.parse(e.message).map {|msg| t("proforma_errors.#{msg}", default: msg) }.join('
')
+ redirect_to :root, danger: JSON.parse(e.message).map {|msg| t("proforma_errors.#{msg}", default: msg) }.join('
'), status: :see_other
end
def share
if share_message.save
- redirect_to @collection, notice: t('.success_notice')
+ redirect_to @collection, notice: t('.success_notice'), status: :see_other
else
- redirect_to @collection, alert: share_message.errors.full_messages.join(', ')
+ redirect_to @collection, alert: share_message.errors.full_messages.join(', '), status: :see_other
end
end
@@ -105,24 +105,28 @@ def view_shared
end
def save_shared # rubocop:disable Metrics/AbcSize
- return redirect_to user_messages_path(current_user), alert: t('.errors.already_member') if @collection.users.include? current_user
+ if @collection.users.include? current_user
+ return redirect_to user_messages_path(current_user),
+ alert: t('.errors.already_member'),
+ status: :see_other
+ end
ActiveRecord::Base.transaction(requires_new: true) do
@collection.users << current_user
message = Message.received_by(current_user).find_by(action: :collection_shared, attachment: @collection)
message.mark_as_deleted(current_user)
message.save!
- redirect_to @collection, notice: t('.success_notice')
+ redirect_to @collection, notice: t('.success_notice'), status: :see_other
end
end
def leave
if @collection.users.one?
@collection.destroy
- redirect_to Collection, notice: t('common.notices.object_deleted', model: Collection.model_name.human)
+ redirect_to Collection, notice: t('common.notices.object_deleted', model: Collection.model_name.human), status: :see_other
else
@collection.users.delete(current_user)
- redirect_to Collection, notice: t('.left_successfully')
+ redirect_to Collection, notice: t('.left_successfully'), status: :see_other
end
end
@@ -134,14 +138,14 @@ def toggle_favorite
current_user.favorite_collections << @collection
flash_message = t('.favorite.added')
end
- redirect_to @collection, notice: flash_message
+ redirect_to @collection, notice: flash_message, status: :see_other
end
private
def create_collection_with_task
if @collection.save
- redirect_to @collection.tasks.first, notice: t('collections.create.success_notice')
+ redirect_to @collection.tasks.first, notice: t('collections.create.success_notice'), status: :see_other
else
flash.now[:alert] = t('collections.create.error')
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 425ec9539..2a5220b59 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -30,31 +30,31 @@ def create
authorize @group
if @group.save
- redirect_to @group, notice: t('common.notices.object_created', model: Group.model_name.human)
+ redirect_to @group, notice: t('common.notices.object_created', model: Group.model_name.human), status: :see_other
else
- render :new
+ render :new, status: :unprocessable_content
end
end
def update
if @group.update(group_params)
- redirect_to @group, notice: t('common.notices.object_updated', model: Group.model_name.human)
+ redirect_to @group, notice: t('common.notices.object_updated', model: Group.model_name.human), status: :see_other
else
- render :edit
+ render :edit, status: :unprocessable_content
end
end
def destroy
@group.destroy
- redirect_to Group, notice: t('common.notices.object_deleted', model: Group.model_name.human)
+ redirect_to Group, notice: t('common.notices.object_deleted', model: Group.model_name.human), status: :see_other
end
def leave
if @group.last_admin?(current_user)
- redirect_to @group, alert: t('.cannot_leave_alert')
+ redirect_to @group, alert: t('.cannot_leave_alert'), status: :see_other
else
@group.users.delete(current_user)
- redirect_to Group, notice: t('.success_notice')
+ redirect_to Group, notice: t('.success_notice'), status: :see_other
end
end
@@ -66,13 +66,13 @@ def request_access
AccessRequestMailer.with(user: current_user, admin:, group: @group).send_access_request.deliver_later
end
@group.add(current_user, role: :applicant)
- redirect_to Group
+ redirect_to Group, status: :see_other
end
def remove_task
task = Task.find(params[:task])
@group.tasks.delete(task)
- redirect_to @group, notice: t('common.notices.object_removed', model: Task.model_name.human)
+ redirect_to @group, notice: t('common.notices.object_removed', model: Task.model_name.human), status: :see_other
end
def grant_access
@@ -80,12 +80,12 @@ def grant_access
send_message(@user, :group_approval)
@group.messages.where(sender: @user, recipient: current_user, action: :group_request).destroy_all
- redirect_to @group, notice: t('.success_notice')
+ redirect_to @group, notice: t('.success_notice'), status: :see_other
end
def delete_from_group
@group.users.delete(@user)
- redirect_to @group, notice: t('.success_notice')
+ redirect_to @group, notice: t('.success_notice'), status: :see_other
end
def deny_access
@@ -93,17 +93,17 @@ def deny_access
send_message(@user, :group_rejection)
@group.messages.where(sender: @user, recipient: current_user, action: :group_request).destroy_all
- redirect_to @group, notice: t('.success_notice')
+ redirect_to @group, notice: t('.success_notice'), status: :see_other
end
def make_admin
@group.make_admin(@user)
- redirect_to @group, notice: t('.success_notice')
+ redirect_to @group, notice: t('.success_notice'), status: :see_other
end
def demote_admin
@group.demote_admin(@user)
- redirect_to @group, notice: t('.success_notice')
+ redirect_to @group, notice: t('.success_notice'), status: :see_other
end
private
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 55403ae31..a0e3463e3 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -33,9 +33,9 @@ def create
authorize @message
if @message.save
- redirect_to user_messages_path(@user), notice: t('.sent_successfully')
+ redirect_to user_messages_path(@user), notice: t('.sent_successfully'), status: :see_other
else
- render :new
+ render :new, status: :unprocessable_content
end
end
@@ -44,9 +44,9 @@ def destroy
if @message.save
redirect_to user_messages_path(@user, option: params[:option]),
- notice: t('common.notices.object_deleted', model: Message.model_name.human)
+ notice: t('common.notices.object_deleted', model: Message.model_name.human), status: :see_other
else
- redirect_to user_messages_path(@user, option: params[:option]), alert: t('.error')
+ redirect_to user_messages_path(@user, option: params[:option]), alert: t('.error'), status: :see_other
end
end
diff --git a/app/controllers/task_contributions_controller.rb b/app/controllers/task_contributions_controller.rb
index 122071a74..1a305091f 100644
--- a/app/controllers/task_contributions_controller.rb
+++ b/app/controllers/task_contributions_controller.rb
@@ -9,18 +9,18 @@ class TaskContributionsController < ApplicationController
def approve_changes
if @task.apply_contribution(@task_contribution)
TaskContributionMailer.with(task_contrib: @task_contribution).send_approval_info.deliver_later
- redirect_to @task, notice: t('.success')
+ redirect_to @task, notice: t('.success'), status: :see_other
else
- redirect_to [@task, @task_contribution], alert: t('.error')
+ redirect_to [@task, @task_contribution], alert: t('.error'), status: :see_other
end
end
def discard_changes
duplicate = @task_contribution.decouple
if duplicate
- redirect_to duplicate, notice: t('.success')
+ redirect_to duplicate, notice: t('.success'), status: :see_other
else
- redirect_to [@task, @task_contribution], alert: t('.error')
+ redirect_to [@task, @task_contribution], alert: t('.error'), status: :see_other
end
end
@@ -28,9 +28,9 @@ def reject_changes
duplicate = @task_contribution.decouple
if duplicate
TaskContributionMailer.with(task_contrib: @task_contribution, duplicate:).send_rejection_info.deliver_later
- redirect_to @task, notice: t('.success')
+ redirect_to @task, notice: t('.success'), status: :see_other
else
- redirect_to [@task, @task_contribution], alert: t('.error')
+ redirect_to [@task, @task_contribution], alert: t('.error'), status: :see_other
end
end
@@ -79,20 +79,25 @@ def create # rubocop:disable Metrics/AbcSize
if @task_contribution.save(context: :force_validations)
TaskContributionMailer.with(task_contrib: @task_contribution).send_contribution_request.deliver_later
- redirect_to [@task, @task_contribution], notice: t('common.notices.object_created', model: TaskContribution.model_name.human)
+ redirect_to [@task, @task_contribution],
+ notice: t('common.notices.object_created', model: TaskContribution.model_name.human),
+ status: :see_other
else
@task = @task_contribution.suggestion
- render 'tasks/new'
+ render 'tasks/new', status: :unprocessable_content
end
end
def update
@task_contribution.suggestion.assign_attributes(task_params.except(:parent_uuid))
if @task_contribution.save(context: :force_validations)
- redirect_to [@task, @task_contribution], notice: t('common.notices.object_updated', model: TaskContribution.model_name.human)
+ redirect_to [@task, @task_contribution],
+ notice: t('common.notices.object_updated', model: TaskContribution.model_name.human),
+ status: :see_other
else
@task = @task_contribution.suggestion
- render 'tasks/edit', danger: t('common.errors.changes_not_saved', model: TaskContribution.model_name.human)
+ render 'tasks/edit', danger: t('common.errors.changes_not_saved', model: TaskContribution.model_name.human),
+ status: :unprocessable_content
end
end
diff --git a/app/controllers/task_files_controller.rb b/app/controllers/task_files_controller.rb
index d1d490ac9..47e99e355 100644
--- a/app/controllers/task_files_controller.rb
+++ b/app/controllers/task_files_controller.rb
@@ -4,7 +4,7 @@ class TaskFilesController < ApplicationController
before_action :load_and_authorize_task_file
def download_attachment
- redirect_to rails_blob_path(@task_file.attachment, disposition: 'attachment')
+ redirect_to rails_blob_path(@task_file.attachment, disposition: 'attachment'), status: :see_other
end
def extract_text_data
diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb
index 28e6b981f..81105a8a0 100644
--- a/app/controllers/tasks_controller.rb
+++ b/app/controllers/tasks_controller.rb
@@ -27,9 +27,9 @@ def index
def duplicate
new_entry = @task.clean_duplicate(current_user)
if new_entry.save(context: :force_validations)
- redirect_to new_entry, notice: t('common.notices.object_duplicated', model: Task.model_name.human)
+ redirect_to new_entry, notice: t('common.notices.object_duplicated', model: Task.model_name.human), status: :see_other
else
- redirect_to @task, alert: t('.error_alert')
+ redirect_to @task, alert: t('.error_alert'), status: :see_other
end
end
@@ -57,9 +57,9 @@ def create
authorize @task
if @task.save(context: :force_validations)
- redirect_to @task, notice: t('common.notices.object_created', model: Task.model_name.human)
+ redirect_to @task, notice: t('common.notices.object_created', model: Task.model_name.human), status: :see_other
else
- render :new
+ render :new, status: :unprocessable_content
end
end
@@ -67,23 +67,23 @@ def update
@task.assign_attributes(task_params)
TaskService::HandleGroups.call(user: current_user, task: @task, group_tasks_params:)
if @task.save(context: :force_validations)
- redirect_to @task, notice: t('common.notices.object_updated', model: Task.model_name.human)
+ redirect_to @task, notice: t('common.notices.object_updated', model: Task.model_name.human), status: :see_other
else
- render :edit
+ render :edit, status: :unprocessable_content
end
end
def destroy
@task.destroy!
- redirect_to Task, notice: t('common.notices.object_deleted', model: Task.model_name.human)
+ redirect_to Task, notice: t('common.notices.object_deleted', model: Task.model_name.human), status: :see_other
end
def add_to_collection
collection = Collection.find(params[:collection])
if collection.add_task(@task)
- redirect_to @task, notice: t('.success_notice')
+ redirect_to @task, notice: t('.success_notice'), status: :see_other
else
- redirect_to @task, alert: t('.error')
+ redirect_to @task, alert: t('.error'), status: :see_other
end
end
@@ -91,7 +91,7 @@ def download
zip_file = ProformaService::ExportTask.call(task: @task, options: {version: params[:version]})
send_data(zip_file.string, type: 'application/zip', filename: "task_#{@task.id}.zip", disposition: 'attachment')
rescue ProformaXML::PostGenerateValidationError => e
- redirect_to :root, danger: JSON.parse(e.message).map {|msg| t("proforma_errors.#{msg}", default: msg) }.join('
')
+ redirect_to :root, danger: JSON.parse(e.message).map {|msg| t("proforma_errors.#{msg}", default: msg) }.join('
'), status: :see_other
end
def import_start # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -213,7 +213,7 @@ def generate_test
rescue Gpt::Error => e
flash.now[:alert] = e.localized_message
ensure
- redirect_to @task
+ redirect_to @task, status: :see_other
end
private
diff --git a/app/controllers/users/nbp_wallet_controller.rb b/app/controllers/users/nbp_wallet_controller.rb
index a05cffa66..7ca26a317 100644
--- a/app/controllers/users/nbp_wallet_controller.rb
+++ b/app/controllers/users/nbp_wallet_controller.rb
@@ -8,14 +8,14 @@ class NbpWalletController < ApplicationController
def connect
if Enmeshed::Relationship.pending_for(@provider_uid).present?
- redirect_to nbp_wallet_finalize_users_path and return
+ redirect_to(nbp_wallet_finalize_users_path, status: :see_other) and return
end
@relationship_template = Enmeshed::RelationshipTemplate.create!(nbp_uid: @provider_uid)
rescue Enmeshed::ConnectorError, Faraday::Error => e
Sentry.capture_exception(e)
Rails.logger.debug { e }
- redirect_to new_user_registration_path, alert: t('common.errors.generic_try_later')
+ redirect_to new_user_registration_path, alert: t('common.errors.generic_try_later'), status: :see_other
end
def qr_code
@@ -32,7 +32,7 @@ def relationship_status
rescue Enmeshed::ConnectorError, Faraday::Error => e
Sentry.capture_exception(e)
Rails.logger.debug { e }
- redirect_to nbp_wallet_connect_users_path, alert: t('common.errors.generic')
+ redirect_to nbp_wallet_connect_users_path, alert: t('common.errors.generic'), status: :see_other
end
def finalize
@@ -64,7 +64,7 @@ def accept_and_create_user(relationship) # rubocop:disable Metrics/AbcSize
if relationship.accept!
user.send_confirmation_instructions
session.clear # Clear the session to prevent the user from accessing the NBP Wallet page again
- redirect_to home_index_path, notice: t('devise.registrations.signed_up_but_unconfirmed')
+ redirect_to home_index_path, notice: t('devise.registrations.signed_up_but_unconfirmed'), status: :see_other
else
abort_and_refresh(relationship)
raise ActiveRecord::Rollback
@@ -80,7 +80,7 @@ def abort_and_refresh(relationship, reason = t('common.errors.generic'))
Sentry.capture_exception(e)
Rails.logger.debug { e }
ensure
- redirect_to nbp_wallet_connect_users_path, alert: reason
+ redirect_to nbp_wallet_connect_users_path, alert: reason, status: :see_other
end
def require_user!
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
index ee71fe5b6..473610770 100644
--- a/app/controllers/users/omniauth_callbacks_controller.rb
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -48,7 +48,7 @@ def deauthorize # rubocop:disable Metrics/AbcSize
# i18n-tasks-use t('users.omniauth_callbacks.failure_deauthorize')
set_flash(:alert, '.failure_deauthorize', reason: identity.errors.full_messages.join(', '))
end
- redirect_to edit_user_registration_path
+ redirect_to edit_user_registration_path, status: :see_other
end
private
@@ -89,7 +89,7 @@ def sign_in_with_identity
def register_new_user # rubocop:disable Metrics/AbcSize
if omniauth_provider == 'nbp'
# go through NBP wallet connection process to create new account
- redirect_to nbp_wallet_connect_users_path
+ redirect_to nbp_wallet_connect_users_path, status: :see_other
else
user = User.new_from_omniauth(omniauth_user_info, omniauth_provider, provider_uid)
user.skip_confirmation!
@@ -99,7 +99,7 @@ def register_new_user # rubocop:disable Metrics/AbcSize
sign_in_and_redirect user, event: :authentication
else
set_flash(:alert, 'devise.omniauth_callbacks.failure', reason: user.errors.full_messages.join(', '))
- redirect_to new_user_registration_url
+ redirect_to new_user_registration_url, status: :see_other
end
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 5a6c053f5..4949a901d 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -5,16 +5,16 @@ class UsersController < ApplicationController
rescue_from Pundit::NotAuthorizedError do |_exception|
if current_user
- redirect_to({id: current_user.id}, alert: t('common.errors.not_authorized'))
+ redirect_to({id: current_user.id}, alert: t('common.errors.not_authorized'), status: :see_other)
else
- redirect_to :root, alert: t('common.errors.not_authorized')
+ redirect_to :root, alert: t('common.errors.not_authorized'), status: :see_other
end
end
rescue_from ActiveRecord::RecordNotFound do |_exception|
# leak no information whether a user exists or not if the accessing user is not an admin
redirect_to({id: current_user.id},
- alert: current_user.role == 'admin' ? t('common.errors.not_found_error') : t('common.errors.not_authorized'))
+ alert: current_user.role == 'admin' ? t('common.errors.not_found_error') : t('common.errors.not_authorized'), status: :see_other)
end
def show; end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index ae0eca119..77e1d651d 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -51,10 +51,10 @@ import locales from "./generated/locales.json";
Promise.all(
Object.keys(locales).map(locale => import(`select2/dist/js/i18n/${locale}`))
).then(() => {
- // Since there is a race condition between the locales and turbolinks,
- // we don't know whether the locales are loaded before or after turbolinks.
+ // Since there is a race condition between the locales and Turbo,
+ // we don't know whether the locales are loaded before or after Turbo.
// Therefore, we trigger the event in both cases.
- $(document).on('turbolinks:load', () =>
+ $(document).on('turbo-migration:load', () =>
$(document).trigger('select2:locales:loaded')
);
$(document).trigger('select2:locales:loaded')
@@ -85,3 +85,7 @@ import "ace-builds/src-noconflict/ext-language_tools"; // Enable autocompletion
import "ace-builds/src-noconflict/ext-modelist"; // Enable language mode detection
ace.config.set("useStrictCSP", true); // Enable strict CSP mode
window.ace = ace; // Publish ace in global namespace
+
+// Turbo
+import '@hotwired/turbo-rails';
+import './turbo-migration';
diff --git a/app/javascript/tooltips.js b/app/javascript/tooltips.js
new file mode 100644
index 000000000..da77fea64
--- /dev/null
+++ b/app/javascript/tooltips.js
@@ -0,0 +1,11 @@
+let tooltipList = [];
+
+export function initializeTooltips() {
+ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
+ tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
+}
+
+export function destroyTooltips() {
+ tooltipList.map(tooltip => tooltip.dispose());
+ tooltipList = [];
+}
diff --git a/app/javascript/turbo-migration.js b/app/javascript/turbo-migration.js
new file mode 100644
index 000000000..ad57ec6d3
--- /dev/null
+++ b/app/javascript/turbo-migration.js
@@ -0,0 +1,63 @@
+import { initializeTooltips, destroyTooltips } from './tooltips';
+
+// `turbo:load` is dispatched earlier than the previous `turbolinks:load` event.
+// This is causing issues for our migration, since some assets are not fully loaded
+// when the event is dispatched. To ensure that the DOM content is fully rendered,
+// we use `requestAnimationFrame` to ensures that the DOM content is completely painted
+// before dispatching a custom event `turbo-migration:load`.
+//
+// Further, we need to ensure that the `turbo-migration:load` event is only processed after
+// Sprockets has loaded, since it would miss the event otherwise.
+//
+// We should remove this workaround once we fully migrated to Turbo and dropped Sprockets.
+
+let sprocketsLoaded = false;
+const sprocketsLoadQueue = [];
+const turboRenderQueue = [];
+
+document.addEventListener('turbo:load', (event) => {
+ sprocketsLoaded ? forwardTurboLoad(event) : sprocketsLoadQueue.push(event);
+});
+
+// Wait for Sprockets to load before processing queued Turbo events
+document.addEventListener('sprockets:load', () => {
+ sprocketsLoaded = true;
+ flushQueue(sprocketsLoadQueue);
+});
+
+// Handle failed form submissions by waiting for `turbo:render` events
+document.addEventListener('turbo:submit-end', (event) => {
+ if (!event.detail.success) {
+ // If the form submission was _not_ successful, we need to re-initialize JavaScript elements.
+ // This is necessary since Turbo does not dispatch a `turbo:load` event in this case.
+ turboRenderQueue.push(event);
+ }
+});
+
+document.addEventListener('turbo:render', () => {
+ if (sprocketsLoaded) {
+ flushQueue(turboRenderQueue);
+ } else {
+ // In the unlikely case that Sprockets isn't ready yet, we queue the events.
+ sprocketsLoadQueue.push(...turboRenderQueue);
+ turboRenderQueue.length = 0;
+ }
+});
+
+function forwardTurboLoad(event) {
+ requestAnimationFrame(() => {
+ const delayedEvent = new CustomEvent('turbo-migration:load', { detail: { ...event.detail } });
+ document.dispatchEvent(delayedEvent);
+
+ initializeTooltips();
+ });
+}
+
+const flushQueue = (queue) => {
+ queue.forEach(forwardTurboLoad);
+ queue.length = 0;
+};
+
+document.addEventListener('turbo:visit', (event) => {
+ destroyTooltips();
+})
diff --git a/app/views/account_links/show.html.slim b/app/views/account_links/show.html.slim
index dd5a40268..7ca213c25 100644
--- a/app/views/account_links/show.html.slim
+++ b/app/views/account_links/show.html.slim
@@ -39,7 +39,7 @@
= link_to edit_user_account_link_path(@user, @account_link), class: 'btn btn-important' do
i.fa-solid.fa-pen-to-square
=< t('common.button.edit')
- = link_to user_account_link_path(@user, @account_link), class: 'btn btn-important', method: :delete, data: {confirm: t('common.sure')} do
+ = button_to user_account_link_path(@user, @account_link), class: 'btn btn-important h-100', method: :delete, data: {confirm: t('common.sure')} do
i.fa-solid.fa-trash-can.text-danger
=< t('common.button.delete')
= link_to t('common.button.back'), user_path(@user), class: 'btn btn-important'
diff --git a/app/views/application/_locale_selector.html.slim b/app/views/application/_locale_selector.html.slim
index 0882da2dd..51ad3b873 100644
--- a/app/views/application/_locale_selector.html.slim
+++ b/app/views/application/_locale_selector.html.slim
@@ -4,4 +4,4 @@ li.nav-item.dropdown
span.caret
ul.dropdown-menu.rounded-0 role='menu'
- I18n.available_locales.sort_by {|locale| t("common.locales.#{locale}") }.each do |locale|
- li = link_to(t("common.locales.#{locale}"), AuthenticatedUrlHelper.add_query_parameters(request.url, locale:), 'data-turbolinks' => 'false', class: 'dropdown-item')
+ li = link_to(t("common.locales.#{locale}"), AuthenticatedUrlHelper.add_query_parameters(request.url, locale:), class: 'dropdown-item')
diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim
index d7525aa8a..a473613b2 100644
--- a/app/views/application/_navigation.html.slim
+++ b/app/views/application/_navigation.html.slim
@@ -5,6 +5,6 @@
= render('navigation_collection_link', model: Group, cached: true)
- if current_user.role == 'admin'
li.nav-item
- = link_to rails_admin.dashboard_path, 'data-turbolinks': false, class: 'nav-link px-3' do
+ = link_to rails_admin.dashboard_path, 'data-turbo': false, class: 'nav-link px-3' do
i.fa-solid.fa-lock
=< t('.rails_admin')
diff --git a/app/views/application/_session.html.slim b/app/views/application/_session.html.slim
index 3f221aac8..6cef15bb0 100644
--- a/app/views/application/_session.html.slim
+++ b/app/views/application/_session.html.slim
@@ -14,7 +14,7 @@
- if current_user.unread_messages_count != '0'
span.menu-number
= current_user.unread_messages_count
- li.nav-item = link_to t('.button.log_out'), destroy_user_session_path, method: :delete, class: 'nav-link px-3'
+ li.nav-item = button_to t('.button.log_out'), destroy_user_session_path, method: :delete, class: 'nav-link px-3', data: {turbo: false}
- else
= render('navigation_collection_link', target: :new_user_registration_path, text: t('common.button.sign_up'), icon: 'fa-user', cached: true)
= render('navigation_collection_link', target: :new_user_session_path, text: t('common.button.log_in'), icon: 'fa-right-to-bracket', cached: true)
diff --git a/app/views/collections/_form.html.slim b/app/views/collections/_form.html.slim
index 4c335523f..a2881b23a 100644
--- a/app/views/collections/_form.html.slim
+++ b/app/views/collections/_form.html.slim
@@ -1,11 +1,8 @@
- content_for :head do
- // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326.
- Otherwise, code might not be highlighted correctly (race condition)
- meta name='turbolinks-visit-control' content='reload'
- append_javascript_pack_tag('toast-ui')
- append_stylesheet_pack_tag('toast-ui')
-= form_for @collection, builder: MarkdownFormBuilder do |f|
+= form_for @collection, builder: MarkdownFormBuilder, data: {turbo: false} do |f|
.my-4
= render('shared/form_errors', object: @collection)
.form-group
@@ -55,7 +52,7 @@
i.fa-solid.fa-folder-open
=< t('.button.save')
- if @collection.tasks.present?
- = link_to remove_all_collection_path(@collection), method: :patch, data: {confirm: t('common.sure')}, class: 'btn btn-important' do
+ = button_to remove_all_collection_path(@collection), method: :patch, data: {confirm: t('common.sure')}, class: 'btn btn-important h-100' do
i.fa-solid.fa-xmark.text-danger
=< t('.button.remove_all')
= link_to :back, class: 'btn btn-important' do
diff --git a/app/views/collections/show.html.slim b/app/views/collections/show.html.slim
index 318e9adb9..45caba4a8 100644
--- a/app/views/collections/show.html.slim
+++ b/app/views/collections/show.html.slim
@@ -74,7 +74,7 @@
.col-md-12.mb-4
.actions.btn-group role='group'
- if action_name == 'view_shared'
- = link_to t('.button.collaborate'), save_shared_collection_path(@collection), method: 'post', class: 'btn btn-important'
+ = button_to t('.button.collaborate'), save_shared_collection_path(@collection), method: 'post', class: 'btn btn-important h-100'
= link_to t('common.button.back'), user_messages_path(current_user), class: 'btn btn-important'
- else
- if policy(@collection).edit?
@@ -120,9 +120,9 @@
i.fa-solid.fa-pen-to-square
=< t('common.button.edit')
- if @collection.users.include? current_user
- = link_to leave_collection_path(@collection), class: 'btn btn-important', method: :post, data: {confirm: @collection.users.size == 1 ? t('.leave.deletion_warning') : t('common.sure')} do
+ = button_to leave_collection_path(@collection), class: 'btn btn-important h-100', method: :post, data: {confirm: @collection.users.size == 1 ? t('.leave.deletion_warning') : t('common.sure')} do
= @collection.users.size == 1 ? t('.button.delete') : t('.button.leave')
- = link_to toggle_favorite_collection_path(@collection), class: 'btn btn-important', method: :post do
+ = button_to toggle_favorite_collection_path(@collection), class: 'btn btn-important h-100', method: :post do
- if current_user.favorite_collections.include?(@collection)
i.fa-regular.fa-star
=< t('.button.unfavorite')
diff --git a/app/views/comments/_edit.html.slim b/app/views/comments/_edit.html.slim
index 8c4d2552e..d1abec60f 100644
--- a/app/views/comments/_edit.html.slim
+++ b/app/views/comments/_edit.html.slim
@@ -7,6 +7,6 @@
= button_tag type: 'submit', class: 'btn btn-light btn-sm' do
i.fa-solid.fa-floppy-disk
=< t('common.button.save')
- = link_to task_comment_path(@task, @comment), class: 'btn btn-light btn-sm', method: :delete, remote: true, data: {confirm: t('common.sure')}
+ = button_to task_comment_path(@task, @comment), class: 'btn btn-light btn-sm', method: :delete, remote: true, data: {confirm: t('common.sure')}
i.fa-solid.fa-trash-can
=< t('common.button.delete')
diff --git a/app/views/comments/_task_comments.html.slim b/app/views/comments/_task_comments.html.slim
index 81f25dd4a..8aa3479bf 100644
--- a/app/views/comments/_task_comments.html.slim
+++ b/app/views/comments/_task_comments.html.slim
@@ -20,7 +20,7 @@
= link_to edit_task_comment_path(@task, comment), class: 'btn btn-light btn-sm', remote: true do
i.fa-solid.fa-pen-to-square
=< t('common.button.edit')
- = link_to task_comment_path(@task, comment), class: 'btn btn-light btn-sm', method: :delete, remote: true, data: {confirm: t('common.sure')}
+ = button_to task_comment_path(@task, comment), class: 'btn btn-light btn-sm', method: :delete, remote: true, data: {confirm: t('common.sure')}
i.fa-solid.fa-trash-can
=< t('common.button.delete')
.more-btn-wrapper
diff --git a/app/views/groups/_groups.html.slim b/app/views/groups/_groups.html.slim
index 64a9c36ce..8ece7be3c 100644
--- a/app/views/groups/_groups.html.slim
+++ b/app/views/groups/_groups.html.slim
@@ -34,11 +34,11 @@
- if policy(group).edit?
= link_to edit_group_path(group), class: 'btn btn-light', data: {bs_toggle: 'tooltip', bs_placement: 'top', bs_title: t('common.button.edit')} do
i.fas.fa-edit aria-hidden="true"
- = link_to group, class: 'btn btn-light', method: :delete, data: {bs_toggle: 'tooltip', bs_placement: 'top', bs_title: t('common.button.delete')} do
+ = button_to group, class: 'btn btn-light', method: :delete, data: {bs_toggle: 'tooltip', bs_placement: 'top', bs_title: t('common.button.delete')} do
i.fas.fa-trash-alt aria-hidden="true"
- elsif group.applicants.include?(current_user)
= t('.wait_to_get_accepted')
- else
- = link_to t('groups.shared.button.request_membership'), request_access_group_path(group), method: :post, class: 'btn btn-light'
+ = button_to t('groups.shared.button.request_membership'), request_access_group_path(group), method: :post, class: 'btn btn-light'
= render('shared/pagination', collection: @groups)
diff --git a/app/views/groups/show.html.slim b/app/views/groups/show.html.slim
index 83c594daa..a02ac1216 100644
--- a/app/views/groups/show.html.slim
+++ b/app/views/groups/show.html.slim
@@ -16,7 +16,7 @@
.table-list
=> admin.email
- if policy(@group).demote_admin? && @group.admins.length > 1
- = link_to demote_admin_group_path(group: @group, user: admin), method: :post, class: 'btn btn-light btn-sm', data: {confirm: admin == current_user ? t('.demote_yourself.confirm') : t('.demote_admin.confirm')} do
+ = button_to demote_admin_group_path(group: @group, user: admin), method: :post, form_class: 'd-inline-block', class: 'btn btn-light btn-sm', data: {confirm: admin == current_user ? t('.demote_yourself.confirm') : t('.demote_admin.confirm')} do
i.fa-solid.fa-arrow-down
=< (admin == current_user ? t('.button.demote_yourself') : t('.button.demote_admin'))
@@ -30,10 +30,10 @@
=> member.email
- if policy(@group).edit?
- if @group.group_membership_for(member).role_confirmed_member?
- = link_to make_admin_group_path(group: @group, user: member), method: :post, class: 'btn btn-light btn-sm' do
+ = button_to make_admin_group_path(group: @group, user: member), method: :post, form_class: 'd-inline-block', class: 'btn btn-light btn-sm' do
i.fa-solid.fa-plus
=< t('.button.make_admin')
- = link_to delete_from_group_group_path(group: @group, user: member), method: :patch, class: 'btn btn-light btn-sm' do
+ = button_to delete_from_group_group_path(group: @group, user: member), method: :patch, form_class: 'd-inline-block', class: 'btn btn-light btn-sm' do
i.fa-solid.fa-xmark
=< t('common.button.remove')
- else
@@ -50,10 +50,10 @@
.table-list
=> user.email
.btn-group.float-end
- = link_to grant_access_group_path(group: @group, user:), method: :post, class: 'btn btn-light btn-sm' do
+ = button_to grant_access_group_path(group: @group, user:), method: :post, form_class: 'd-inline-block', class: 'btn btn-light btn-sm' do
i.fa-solid.fa-plus
=< t('.button.grant_access')
- = link_to deny_access_group_path(group: @group, user:), method: :post, class: 'btn btn-light btn-sm' do
+ = button_to deny_access_group_path(group: @group, user:), method: :post, form_class: 'd-inline-block', class: 'btn btn-light btn-sm' do
i.fa-solid.fa-xmark
=< t('.button.deny_access')
- else
@@ -67,7 +67,7 @@
.btn-group
= link_to task.title, task_path(task), class: 'btn btn-light'
- if policy(@group).remove_task?
- = link_to remove_task_group_path(task:), method: :patch, class: 'btn btn-light' do
+ = button_to remove_task_group_path(task:), method: :patch, form_class: 'd-inline-block', class: 'btn btn-light' do
i.fa-solid.fa-xmark.text-secondary
- else
= t('.has_no_tasks')
@@ -76,14 +76,14 @@
.col-md-12.mb-4
.actions.btn-group role='group'
- if policy(@group).request_access?
- = link_to t('groups.shared.button.request_membership'), request_access_group_path(@group), method: :post, class: 'btn btn-important'
+ = button_to t('groups.shared.button.request_membership'), request_access_group_path(@group), method: :post, class: 'btn btn-important h-100'
- if policy(@group).edit?
= link_to edit_group_path(@group), class: 'btn btn-important' do
i.fa-solid.fa-pen-to-square
=< t('common.button.edit')
- = link_to group_path(@group), class: 'btn btn-important', method: :delete, data: {confirm: t('common.sure')} do
+ = button_to group_path(@group), class: 'btn btn-important h-100', method: :delete, data: {confirm: t('common.sure')} do
i.fa-solid.fa-trash-can.text-danger
=< t('.button.delete_group')
- if policy(@group).leave?
- = link_to t('.button.leave'), leave_group_path(@group), method: :post, class: "leave-btn btn btn-important #{'disabled' if @group.last_admin?(current_user)}", data: {confirm: t('common.sure')}
+ = button_to t('.button.leave'), leave_group_path(@group), method: :post, class: "leave-btn btn btn-important h-100 #{'disabled' if @group.last_admin?(current_user)}", data: {confirm: t('common.sure')}
= link_to t('common.button.back'), :back, class: 'btn btn-important'
diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim
index 737122c38..7e6652e04 100644
--- a/app/views/layouts/application.html.slim
+++ b/app/views/layouts/application.html.slim
@@ -11,10 +11,10 @@ html lang=I18n.locale data-default-locale=I18n.default_locale
= favicon_link_tag('/icon.svg', type: 'image/svg+xml')
= favicon_link_tag('/icon.png', rel: 'apple-touch-icon', type: 'image/png')
= tag.link rel: 'manifest', href: pwa_manifest_path
- = stylesheet_pack_tag('stylesheets', "multilang_#{I18n.locale}", media: 'all', 'data-turbolinks-track': 'reload', integrity: true, crossorigin: 'anonymous')
- = stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': 'reload', integrity: true, crossorigin: 'anonymous')
- = javascript_pack_tag('application', 'data-turbolinks-track': 'reload', defer: false, integrity: true, crossorigin: 'anonymous')
- = javascript_include_tag('application', 'data-turbolinks-track': 'reload', integrity: true, crossorigin: 'anonymous')
+ = stylesheet_pack_tag('stylesheets', "multilang_#{I18n.locale}", media: 'all', 'data-turbo-track': 'reload', integrity: true, crossorigin: 'anonymous')
+ = stylesheet_link_tag('application', media: 'all', 'data-turbo-track': 'reload', integrity: true, crossorigin: 'anonymous')
+ = javascript_pack_tag('application', 'data-turbo-track': 'reload', defer: false, integrity: true, crossorigin: 'anonymous')
+ = javascript_include_tag('application', 'data-turbo-track': 'reload', defer: true, integrity: true, crossorigin: 'anonymous')
= yield(:head)
= csrf_meta_tags
/= csp_meta_tag
diff --git a/app/views/messages/index.html.slim b/app/views/messages/index.html.slim
index 607fbc63b..7871fed3d 100644
--- a/app/views/messages/index.html.slim
+++ b/app/views/messages/index.html.slim
@@ -45,13 +45,13 @@ br
- if message.attachment.present?
- if message.action_collection_shared?
= link_to t('.button.view'), view_shared_collection_path(message.attachment, user: message.sender), class: 'btn btn-light btn-sm'
- = link_to t('.button.save_collection'), save_shared_collection_path(message.attachment), method: 'post', class: 'btn btn-light btn-sm'
+ = button_to t('.button.save_collection'), save_shared_collection_path(message.attachment), method: 'post', class: 'btn btn-light btn-sm'
- if message.action_group_request?
= link_to t('.button.view'), group_path(message.attachment), class: 'btn btn-light btn-sm'
- = link_to grant_access_group_path(message.attachment, user: message.sender.id), method: :post, class: 'btn btn-light btn-sm' do
+ = button_to grant_access_group_path(message.attachment, user: message.sender.id), method: :post, class: 'btn btn-light btn-sm' do
i.fa-solid.fa-plus.text-success
=< t('.button.grant_access')
- = link_to deny_access_group_path(message.attachment, user: message.sender.id), method: :post, class: 'btn btn-light btn-sm' do
+ = button_to deny_access_group_path(message.attachment, user: message.sender.id), method: :post, class: 'btn btn-light btn-sm' do
i.fa-solid.fa-xmark.text-danger
=< t('.button.deny_access')
- if message.action_group_approval?
@@ -63,7 +63,7 @@ br
= link_to reply_user_messages_path(recipient: message.sender), class: 'btn btn-light btn-sm' do
i.fa-solid.fa-reply
=< t('.button.reply')
- = link_to user_message_path(current_user, message, option: @option), method: :delete, class: 'btn btn-light btn-sm', data: {confirm: t('common.sure')} do
+ = button_to user_message_path(current_user, message, option: @option), method: :delete, class: 'btn btn-light btn-sm', data: {confirm: t('common.sure')} do
i.fa-solid.fa-trash-can
=< t('common.button.delete')
= render('shared/pagination', collection: @messages)
diff --git a/app/views/tasks/_editor.html.slim b/app/views/tasks/_editor.html.slim
index 5f0e7bf1a..1fe8a7569 100644
--- a/app/views/tasks/_editor.html.slim
+++ b/app/views/tasks/_editor.html.slim
@@ -18,7 +18,7 @@ a.btn.btn-main.btn-sm.toggle-input href='#'
- if file.object.persisted? && file.object.attachment.image?
= image_tag file.object.attachment.variant(resize_to_limit: [600, 600])
- elsif file.object.persisted?
- = link_to file.object.attachment.filename, download_attachment_task_file_path(file.object), class: 'btn btn-light border-right-0', 'data-turbolinks' => false
+ = link_to file.object.attachment.filename, download_attachment_task_file_path(file.object), class: 'btn btn-light border-right-0', 'data-turbo' => false
- else
.btn.btn-light.border-right-0.disabled = file.object.attachment.filename
= link_to '#', class: 'btn btn-light reupload-attachment border-left-0 me-1', title: t('.button.upload_a_new_file') do
diff --git a/app/views/tasks/_form.html.slim b/app/views/tasks/_form.html.slim
index e1caa783f..e6b81fb25 100644
--- a/app/views/tasks/_form.html.slim
+++ b/app/views/tasks/_form.html.slim
@@ -1,7 +1,4 @@
- content_for :head do
- // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326.
- Otherwise, code might not be highlighted correctly (race condition)
- meta name='turbolinks-visit-control' content='reload'
- append_javascript_pack_tag('toast-ui')
- append_stylesheet_pack_tag('toast-ui')
@@ -14,7 +11,7 @@
- url = task_task_contribution_path(@task.parent, @task.task_contribution)
- else
- url = nil
- = simple_form_for @task, url:, html: {multipart: true}, builder: MarkdownFormBuilder do |f|
+ = simple_form_for @task, url:, html: {multipart: true}, builder: MarkdownFormBuilder, data: {turbo: false} do |f|
= render('shared/form_errors', object: @task)
fieldset.form-group
legend.toggle-next
diff --git a/app/views/tasks/_tasks.html.slim b/app/views/tasks/_tasks.html.slim
index e0630411b..9a75dc491 100644
--- a/app/views/tasks/_tasks.html.slim
+++ b/app/views/tasks/_tasks.html.slim
@@ -68,7 +68,7 @@
= link_to task.url_for(:edit), class: 'btn btn-light', data: {bs_toggle: 'tooltip', bs_placement: 'top', bs_title: t('common.button.edit')} do
i.fas.fa-edit aria-hidden="true"
- if policy(task).destroy?
- = link_to task.url_for(:destroy), class: 'btn btn-light', method: :delete, data: {bs_toggle: 'tooltip', bs_placement: 'top', bs_title: t('common.button.delete'), confirm: t('common.sure')} do
+ = button_to task.url_for(:destroy), class: 'btn btn-light', method: :delete, data: {bs_toggle: 'tooltip', bs_placement: 'top', bs_title: t('common.button.delete'), confirm: t('common.sure')} do
i.fas.fa-trash-alt aria-hidden="true"
.card-footer.d-flex.align-items-center.pe-2
diff --git a/app/views/tasks/show.html.slim b/app/views/tasks/show.html.slim
index 6d2abafff..eb244fd57 100644
--- a/app/views/tasks/show.html.slim
+++ b/app/views/tasks/show.html.slim
@@ -217,7 +217,7 @@
- if file.attachment.image?
= image_tag(file.attachment.variant(resize_to_limit: [900, 900]))
- else
- = link_to file.attachment.filename, download_attachment_task_file_path(file), 'data-turbolinks': false
+ = link_to file.attachment.filename, download_attachment_task_file_path(file), 'data-turbo': false
.row
.col-auto.row-label
= TaskFile.human_attribute_name('used_by_grader')
@@ -358,7 +358,7 @@
- if file.attachment.image?
= image_tag(file.attachment.variant(resize_to_limit: [900, 900]))
- else
- = link_to file.attachment.filename, download_attachment_task_file_path(file), 'data-turbolinks': false
+ = link_to file.attachment.filename, download_attachment_task_file_path(file), 'data-turbo': false
.row
.col-auto.row-label
= TaskFile.human_attribute_name('used_by_grader')
@@ -459,7 +459,7 @@
- if file.attachment.image?
= image_tag(file.attachment.variant(resize_to_limit: [900, 900]))
- else
- = link_to file.attachment.filename, download_attachment_task_file_path(file), 'data-turbolinks': false
+ = link_to file.attachment.filename, download_attachment_task_file_path(file), 'data-turbo': false
.row
.col-auto.row-label
= TaskFile.human_attribute_name('used_by_grader')
@@ -492,11 +492,11 @@
- current_user.collections.order(created_at: :desc).each do |collection|
li
- if collection.tasks.include?(@task)
- = link_to remove_task_collection_path(collection, task: @task, return_to_task: true), method: :patch, class: 'dropdown-item', data: {confirm: t('.remove_task_from_collection_warning')} do
+ = button_to remove_task_collection_path(collection, task: @task, return_to_task: true), method: :patch, class: 'dropdown-item', data: {confirm: t('.remove_task_from_collection_warning')} do
span.collection-checkmark
= "✓ #{collection.title}"
- else
- = link_to add_to_collection_task_path(collection:), method: :post, class: 'dropdown-item' do
+ = button_to add_to_collection_task_path(collection:), method: :post, class: 'dropdown-item' do
span.collection-checkmark
= collection.title
@@ -529,7 +529,7 @@
- else
div data-bs-toggle='tooltip' title=unavailable_tooltip data-bs-delay=150
- = link_to t('common.button.download_zip'), download_task_path(@task), class: 'btn btn-outline-dark disabled', 'data-turbolinks' => false
+ = link_to t('common.button.download_zip'), download_task_path(@task), class: 'btn btn-outline-dark disabled', 'data-turbo' => false
- if policy(@task).export_external_start?
.dropdown.btn-group
@@ -543,7 +543,7 @@
- else
- current_user.available_account_links.each do |acc_link|
li
- = link_to acc_link.name, export_external_start_task_path(account_link: acc_link), method: :post, remote: true, class: 'dropdown-item export-test'
+ = button_to acc_link.name, export_external_start_task_path(account_link: acc_link), method: :post, remote: true, class: 'dropdown-item export-test'
- else
div data-bs-toggle='tooltip' title=unavailable_tooltip data-bs-delay=150
= button_tag class: 'btn btn-outline-dark dropdown-toggle disabled', 'data-bs-toggle': 'dropdown' do
@@ -587,23 +587,23 @@
.actions.btn-group role='group'
- if policy(@task).edit?
- = link_to @task.url_for(:edit), class: 'btn btn-important', 'data-turbolinks': false do
+ = link_to @task.url_for(:edit), class: 'btn btn-important', 'data-turbo': false do
i.fa-solid.fa-pen-to-square
=< t('common.button.edit')
- if policy(@task).destroy?
- = link_to task_path(@task), class: 'btn btn-important', method: :delete, data: {confirm: t('common.sure')} do
+ = button_to task_path(@task), class: 'btn btn-important h-100', method: :delete, data: {confirm: t('common.sure')} do
i.fa-solid.fa-trash-can
=< t('common.button.delete')
- if policy(@task).approve_changes?
- = link_to @task.url_for(:approve_changes), method: :post, class: 'btn btn-important' do
+ = button_to @task.url_for(:approve_changes), method: :post, class: 'btn btn-important h-100' do
i.fa-solid.fa-check
=< t('common.button.approve')
- if policy(@task).reject_changes?
- = link_to @task.url_for(:reject_changes), method: :post, class: 'btn btn-important' do
+ = button_to @task.url_for(:reject_changes), method: :post, class: 'btn btn-important h-100' do
i.fa-solid.fa-xmark
=< t('common.button.reject')
- if policy(@task).discard_changes?
- = link_to @task.url_for(:discard_changes), method: :post, class: 'btn btn-important' do
+ = button_to @task.url_for(:discard_changes), method: :post, class: 'btn btn-important h-100' do
i.fa-solid.fa-link-slash
=< t('task_contributions.buttons.decouple')
@@ -619,27 +619,27 @@
=< t('task_contributions.buttons.contribute')
- elsif @current_user.nil?
.btn-group data-bs-toggle='tooltip' title=unavailable_tooltip data-bs-delay=150
- = link_to new_task_task_contribution_path(@task), method: :post, class: 'btn btn-important disabled' do
+ = button_to new_task_task_contribution_path(@task), method: :post, class: 'btn btn-important disabled h-100' do
i.fa-regular.fa-clone
=< t('task_contributions.buttons.contribute')
- if policy(@task).duplicate?
- = link_to duplicate_task_path(@task), method: :post, class: 'btn btn-important' do
+ = button_to duplicate_task_path(@task), method: :post, class: 'btn btn-important h-100' do
i.fa-regular.fa-clone
=< t('common.button.duplicate')
- elsif @current_user.nil?
.btn-group data-bs-toggle='tooltip' title=unavailable_tooltip data-bs-delay=150
- = link_to duplicate_task_path(@task), method: :post, class: 'btn btn-important disabled' do
+ = button_to duplicate_task_path(@task), method: :post, class: 'btn btn-important disabled h-100' do
i.fa-regular.fa-clone
=< t('common.button.duplicate')
- if policy(@task).generate_test?
- = link_to generate_test_task_path(@task), method: :post, class: 'btn btn-important' do
+ = button_to generate_test_task_path(@task), method: :post, class: 'btn btn-important h-100' do
i.fa-solid.fa-wand-magic-sparkles
= t('.button.generate_test')
- elsif policy(@task).update?
.btn-group data-bs-toggle='tooltip' title=t('.button.api_key_required') data-bs-delay=150
- = link_to generate_test_task_path(@task), method: :post, class: 'btn btn-important disabled' do
+ = button_to generate_test_task_path(@task), method: :post, class: 'btn btn-important disabled h-100' do
i.fa-solid.fa-wand-magic-sparkles
= t('.button.generate_test')
diff --git a/app/views/users/nbp_wallet/connect.html.slim b/app/views/users/nbp_wallet/connect.html.slim
index d5e84ecdf..c5cfb9f25 100644
--- a/app/views/users/nbp_wallet/connect.html.slim
+++ b/app/views/users/nbp_wallet/connect.html.slim
@@ -12,7 +12,7 @@
class: 'img-fluid pixelated'
.btn.btn-primary.w-100.mt-3 data-behavior='reload-on-click'
= t('.regenerate_code')
- = link_to destroy_user_session_path, method: :delete, class: 'btn btn-outline-danger w-100 mt-3'
+ = button_to destroy_user_session_path, method: :delete, class: 'btn btn-outline-danger w-100 mt-3', data: {turbo: false}
= t('.cancel_registration')
.col-8.col-md-6.mt-3.ms-md-3.mt-md-0
p.fs-6
diff --git a/app/views/users/registrations/edit.html.slim b/app/views/users/registrations/edit.html.slim
index 52a87473b..c7c7a378f 100644
--- a/app/views/users/registrations/edit.html.slim
+++ b/app/views/users/registrations/edit.html.slim
@@ -83,12 +83,12 @@
- provider_name = OmniAuth::Utils.camelize(provider)
- configured_providers = current_user.omniauth_identities.pluck(:omniauth_provider)
- if configured_providers.include?(provider.to_s) && (configured_providers.size > 1 || resource.password_set?)
- = link_to t('.remove_identity', kind: provider_name), omniauth_deauthorize_path(provider), class: 'btn btn-light btn-sm mb-2 me-2', method: :delete
+ = button_to t('.remove_identity', kind: provider_name), omniauth_deauthorize_path(provider), class: 'btn btn-light btn-sm mb-2 me-2', method: :delete
- elsif configured_providers.include?(provider.to_s)
a.btn.btn-light.btn-sm.mb-2.me-2.disabled href='#'
= t('.cannot_remove_last_identity', kind: provider_name)
- else
- = link_to t('.add_identity', kind: provider_name), omniauth_authorize_path(resource_name, provider), class: 'btn btn-light btn-sm mb-2 me-2', method: :post
+ = button_to t('.add_identity', kind: provider_name), omniauth_authorize_path(resource_name, provider), class: 'btn btn-light btn-sm mb-2 me-2', method: :post
.form-group.py-3
.btn-group role='group'
diff --git a/app/views/users/shared/_links.html.slim b/app/views/users/shared/_links.html.slim
index 769086f22..c43e5cd6b 100644
--- a/app/views/users/shared/_links.html.slim
+++ b/app/views/users/shared/_links.html.slim
@@ -21,6 +21,6 @@
- if devise_mapping.omniauthable?
- resource_class.omniauth_providers.each do |provider|
- = link_to t('devise.shared.links.sign_in_with_provider', provider: OmniAuth::Utils.camelize(provider)),
+ = button_to t('devise.shared.links.sign_in_with_provider', provider: OmniAuth::Utils.camelize(provider)),
omniauth_authorize_path(resource_name, provider), method: :post
br
diff --git a/app/views/users/show.html.slim b/app/views/users/show.html.slim
index e7c845dd2..d727ea9f3 100644
--- a/app/views/users/show.html.slim
+++ b/app/views/users/show.html.slim
@@ -81,7 +81,7 @@
= link_to edit_user_account_link_path(current_user, account_link), class: 'btn btn-light btn-sm' do
i.fa-solid.fa-pen-to-square
=< t('common.button.edit')
- = link_to user_account_link_path(current_user, account_link), method: :delete, data: {confirm: t('common.sure')}, class: 'btn btn-light btn-sm' do
+ = button_to user_account_link_path(current_user, account_link), method: :delete, data: {confirm: t('common.sure')}, class: 'btn btn-light btn-sm' do
i.fa-solid.fa-trash-can
=< t('common.button.delete')
- if @user.shared_account_links.any?
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index b9e742e28..9f72df527 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -80,7 +80,7 @@ def self.get_host_source(url)
# The `script_src` directive is only a fallback for browsers not supporting `script_src_elem` and `script_src_attr`.
policy.script_src :self, :unsafe_inline, :report_sample
# Some dependencies add new styles to the DOM dynamically, requiring :unsafe-inline.
- # Currently, these include turbolinks, and vis.js.
+ # Currently, these include turbo, and vis.js.
policy.style_src_elem :self, :unsafe_inline, :report_sample
# We still use some inline styles within the application, and through the ToastUi markdown editor.
policy.style_src_attr :unsafe_inline, :report_sample
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 709995ffb..08f695da4 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -262,14 +262,14 @@
# ==> Navigation configuration
# Lists the formats that should be treated as navigational. Formats like
- # :html, should redirect to the sign in page when the user does not have
+ # :html should redirect to the sign in page when the user does not have
# access, but formats like :xml or :json, should return 401.
#
# If you have any extra navigational formats, like :iphone or :mobile, you
# should add them to the navigational formats lists.
#
# The "*/*" below is required to match Internet Explorer requests.
- # config.navigational_formats = ['*/*', :html]
+ # config.navigational_formats = ['*/*', :html, :turbo_stream]
# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :delete
@@ -305,12 +305,14 @@
# so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = '/my_engine/users/auth'
- # ==> Turbolinks configuration
- # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly:
- #
- # ActiveSupport.on_load(:devise_failure_app) do
- # include Turbolinks::Controller
- # end
+ # ==> Hotwire/Turbo configuration
+ # 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
# ==> Configuration for :registerable
diff --git a/package.json b/package.json
index 2413861e8..3c6bc3964 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"@babel/preset-env": "7.28.0",
"@babel/runtime": "7.27.6",
"@fortawesome/fontawesome-free": "^6.7.2",
+ "@hotwired/turbo-rails": "^8.0.16",
"@popperjs/core": "^2.11.8",
"@sentry/browser": "^9.35.0",
"@toast-ui/editor": "^3.2.2",
diff --git a/spec/controllers/tasks_controller_spec.rb b/spec/controllers/tasks_controller_spec.rb
index e0602fcd5..dda4127c2 100644
--- a/spec/controllers/tasks_controller_spec.rb
+++ b/spec/controllers/tasks_controller_spec.rb
@@ -370,9 +370,9 @@
let(:invalid_attributes) { {title: '', label_names: [not_existing_label_name]} }
let(:not_existing_label_name) { 'some new label' }
- it "re-renders the 'new' template successfully" do
+ it "re-renders the 'new' template successfully with status 422" do
post_request
- expect(response).to have_http_status(:success)
+ expect(response).to have_http_status(:unprocessable_content)
expect(response).to render_template('new')
end
end
diff --git a/spec/requests/collections_spec.rb b/spec/requests/collections_spec.rb
index 5dddc42db..82842b038 100644
--- a/spec/requests/collections_spec.rb
+++ b/spec/requests/collections_spec.rb
@@ -21,9 +21,9 @@
end
describe 'POST /collections' do
- it 'has http 302' do
+ it 'responds with status 303' do
post collections_path, params: {collection: collection_params}
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
@@ -49,23 +49,23 @@
end
describe 'PATCH /collection/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
patch collection_path(collection, collection: collection_params)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'PUT /collection/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
put collection_path(collection, collection: collection_params)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'POST /collection/:id/remove_task/:task_id' do
- it 'has http 302' do
+ it 'responds with status 303' do
patch remove_task_collection_path(collection, collection:, task:)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
end
diff --git a/spec/requests/groups_spec.rb b/spec/requests/groups_spec.rb
index 998dc7c63..402ea0f72 100644
--- a/spec/requests/groups_spec.rb
+++ b/spec/requests/groups_spec.rb
@@ -21,9 +21,9 @@
end
describe 'POST /groups' do
- it 'has http 302' do
+ it 'responds with status 303' do
post groups_path, params: {group: group_params}
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
@@ -49,23 +49,23 @@
end
describe 'PATCH /group/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
patch group_path(group, group: group_params)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'PUT /group/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
put group_path(group, group: group_params)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'DELETE /group/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
delete group_path(group)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
end
diff --git a/spec/requests/tasks_spec.rb b/spec/requests/tasks_spec.rb
index ae176b50c..6edb29ff0 100644
--- a/spec/requests/tasks_spec.rb
+++ b/spec/requests/tasks_spec.rb
@@ -37,9 +37,9 @@
end
describe 'POST /tasks' do
- it 'has http 302' do
+ it 'responds with status 303' do
post tasks_path, params: {task: valid_params, group_tasks: {group_ids: ['']}}
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
@@ -65,23 +65,23 @@
end
describe 'PATCH /task/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
patch task_path(task, task: update_params, group_tasks: {group_ids: ['']})
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'PUT /task/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
put task_path(task, task: update_params, group_tasks: {group_ids: ['']})
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'DELETE /task/:id' do
- it 'has http 302' do
+ it 'responds with status 303' do
delete task_path(task)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
end
diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb
index af994f5ee..c09b445f6 100644
--- a/spec/requests/users_spec.rb
+++ b/spec/requests/users_spec.rb
@@ -7,9 +7,10 @@
context 'when not logged in' do
describe '/users/sign_in' do
- it 'returns http 200' do
- post user_session_path, params: {email: user.email, password: user.password}
- expect(response).to have_http_status(:ok)
+ it 'responds with status 303' do
+ post user_session_path, params: {user: {email: user.email, password: user.password}}
+ expect(response).to have_http_status(:see_other)
+ expect(response).to redirect_to(root_path)
end
end
end
@@ -22,7 +23,7 @@
end
describe 'GET /users/sign_up' do
- it 'returns http 302' do
+ it 'responds with status 302' do
get new_user_registration_path
expect(response).to have_http_status(:found)
end
@@ -43,23 +44,23 @@
end
describe 'PATCH /users' do
- it 'returns http 302' do
+ it 'responds with status 303' do
patch user_registration_path(user, user: user_params, format: :html)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'PUT /users' do
- it 'returns http 302' do
+ it 'responds with status 303' do
put user_registration_path(user, user: user_params, format: :html)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
describe 'DELETE /users' do
- it 'returns http 302' do
+ it 'responds with status 303' do
delete user_registration_path(user, format: :html)
- expect(response).to have_http_status(:found)
+ expect(response).to have_http_status(:see_other)
end
end
end
diff --git a/spec/support/expectations/has_form_button.rb b/spec/support/expectations/has_form_button.rb
new file mode 100644
index 000000000..c449f9484
--- /dev/null
+++ b/spec/support/expectations/has_form_button.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_form_button do |action: nil|
+ match do |page|
+ page.has_selector?(:xpath, ".//form[@action='#{action}']/button")
+ end
+
+ failure_message do |_page|
+ "expected that the page would have a form with the action: #{action}"
+ end
+
+ failure_message_when_negated do |_page|
+ "expected that the page would not have a form button with the action: #{action}"
+ end
+end
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index 9baf7bcff..293d45ced 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -7,7 +7,7 @@ def wait_for_ajax
loop do
sleep 0.1 # Short sleep time to prevent busy waiting
- break if ajax_requests_finished? || (Time.current - start_time) > timeout
+ break if (ajax_requests_finished? && turbo_finished?) || (Time.current - start_time) > timeout
end
end
@@ -16,6 +16,13 @@ def ajax_requests_finished?
# Otherwise, Selenium and the browser driver might crash, preventing further tests from running.
page.evaluate_script('jQuery.active').zero?
end
+
+ def turbo_finished?
+ # Check if Turbo is finished by looking for the absence of the progress bar.
+ if has_css?('.turbo-progress-bar', visible: true, wait: 0.1.seconds)
+ has_no_css?('.turbo-progress-bar')
+ end
+ end
end
RSpec.configure do |config|
diff --git a/spec/system/messages_system_spec.rb b/spec/system/messages_system_spec.rb
index 73acc3a3b..486d7cfc2 100644
--- a/spec/system/messages_system_spec.rb
+++ b/spec/system/messages_system_spec.rb
@@ -20,16 +20,16 @@
let(:view_shared_link) { view_shared_collection_path(collection, user: collection_owner) }
let(:save_shared_link) { save_shared_collection_path(collection) }
- it 'shows the message with a link to the collection' do
+ it 'shows the message with actions to the collection' do
view_inbox
expect(page).to have_link(href: view_shared_link)
- expect(page).to have_link(href: save_shared_link)
+ expect(page).to have_form_button(action: save_shared_link)
end
it 'adds the user to the collection when saving the collection' do
view_inbox
expect do
- find("[href='#{save_shared_link}'").click
+ click_button(I18n.t('messages.index.button.save_collection'))
wait_for_ajax
end.to change { collection.users.reload.include? user }.from(false).to(true)
end
@@ -58,16 +58,16 @@
let(:grant_access_link) { grant_access_group_path(group, user: sender) }
let(:deny_access_link) { deny_access_group_path(group, user: sender) }
- it 'shows the message with links to grant or deny the request' do
+ it 'shows the message with actions to grant or deny the request' do
view_inbox
- expect(page).to have_link(href: grant_access_link)
- expect(page).to have_link(href: deny_access_link)
+ expect(page).to have_form_button(action: grant_access_link)
+ expect(page).to have_form_button(action: deny_access_link)
end
it 'confirms the sender as a group member when clicking the grant access link' do
view_inbox
expect do
- find("[href='#{grant_access_link}'").click
+ click_button(I18n.t('messages.index.button.grant_access'))
wait_for_ajax
end.to change { group.reload.confirmed_member? sender }.from(false).to(true)
end
diff --git a/yarn.lock b/yarn.lock
index 3e06d523c..eef3870a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1304,6 +1304,16 @@ __metadata:
languageName: node
linkType: hard
+"@hotwired/turbo-rails@npm:^8.0.16":
+ version: 8.0.16
+ resolution: "@hotwired/turbo-rails@npm:8.0.16"
+ dependencies:
+ "@hotwired/turbo": "npm:^8.0.13"
+ "@rails/actioncable": "npm:>=7.0"
+ checksum: 10c0/a3781bf5e7c798307754a726b5b66f63e4ec71a74e508549f257b3acf834ffc5c28baaf03b8dcdeeb7b35caa361769e417bbca629dcf873729398773585d35ca
+ languageName: node
+ linkType: hard
+
"@hotwired/turbo@npm:^7.3.0":
version: 7.3.0
resolution: "@hotwired/turbo@npm:7.3.0"
@@ -1311,6 +1321,13 @@ __metadata:
languageName: node
linkType: hard
+"@hotwired/turbo@npm:^8.0.13":
+ version: 8.0.13
+ resolution: "@hotwired/turbo@npm:8.0.13"
+ checksum: 10c0/fc9fd58ce2e006ad2f9e3948cf1ec71f47187ce8115f03e531bab849d0e13abc94cd0067f0888f7064d730b4c1a8212101bfa6d55f6166c6ad2db275e280149a
+ languageName: node
+ linkType: hard
+
"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
@@ -1655,6 +1672,13 @@ __metadata:
languageName: node
linkType: hard
+"@rails/actioncable@npm:>=7.0":
+ version: 8.0.200
+ resolution: "@rails/actioncable@npm:8.0.200"
+ checksum: 10c0/42c861f3b43131ab523d37125e30e802bd5bab98bf8150a780531f82f05177cd0071a75a7f1341eac9de772732625194ac16205a40b82dc567cc05c21c56e412
+ languageName: node
+ linkType: hard
+
"@rails/actioncable@npm:^7.0":
version: 7.2.201
resolution: "@rails/actioncable@npm:7.2.201"
@@ -2745,6 +2769,7 @@ __metadata:
"@babel/preset-env": "npm:7.28.0"
"@babel/runtime": "npm:7.27.6"
"@fortawesome/fontawesome-free": "npm:^6.7.2"
+ "@hotwired/turbo-rails": "npm:^8.0.16"
"@popperjs/core": "npm:^2.11.8"
"@sentry/browser": "npm:^9.35.0"
"@toast-ui/editor": "npm:^3.2.2"