Skip to content

Migrate Turbolinks to Turbo #2998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ gem 'sprockets-rails'
gem 'telegraf'
gem 'terser', require: false
gem 'tubesock', github: 'openhpi/tubesock'
gem 'turbolinks'
gem 'turbo-rails'
gem 'webauthn'
gem 'zxcvbn-ruby', require: 'zxcvbn'

Expand Down
7 changes: 1 addition & 6 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,6 @@ GEM
turbo-rails (2.0.16)
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)
Expand Down Expand Up @@ -750,7 +747,7 @@ DEPENDENCIES
telegraf
terser
tubesock!
turbolinks
turbo-rails
web-console
webauthn
webmock
Expand Down Expand Up @@ -993,8 +990,6 @@ CHECKSUMS
tpm-key_attestation (0.14.1) sha256=7fd4e4653a7afd0a386632ddfb05d10ecfdd47678299c5e69165bc9ae111193f
tubesock (0.2.9)
turbo-rails (2.0.16) sha256=d24e1b60f0c575b3549ecda967e5391027143f8220d837ed792c8d48ea0ea38d
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
Expand Down
6 changes: 5 additions & 1 deletion app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require turbolinks
//= require rails-timeago
//= require locales/jquery.timeago.de.js
//
Expand All @@ -35,3 +34,8 @@
//
// All remaining assets are loaded in alphabetical order
//= 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);
11 changes: 4 additions & 7 deletions app/assets/javascripts/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Array.prototype.includes = function(element) {

window.CodeOcean = {
refresh: function() {
Turbolinks.visit(window.location.pathname);
Turbo.visit(window.location.pathname);
}
};

Expand All @@ -24,7 +24,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();
Expand All @@ -45,7 +45,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'),
Expand All @@ -66,10 +66,7 @@ $(document).on('turbolinks:load', function() {
});
}

// Enable all tooltips
$('[data-bs-toggle="tooltip"]').tooltip();

// Enable sorttable again, as it is disabled otherwise by Turbolinks
// Enable sorttable again, as it is disabled otherwise by Turbo
if (sorttable) {
sorttable.init.done = false;
sorttable.init();
Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/bootstrap-dropdown-submenu.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {

var subMenusSelector = 'ul.dropdown-menu [data-bs-toggle=dropdown]';

Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/channels/la_exercises.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {
if ($.isController('exercises') && $('.teacher_dashboard').isPresent()) {

const exercise_id = $('.teacher_dashboard').data().exerciseId;
Expand All @@ -11,7 +11,7 @@ $(document).on('turbolinks:load', function() {

function addClickEventToRfCEntry($row) {
$row.click(function () {
Turbolinks.visit($(this).data("href"));
Turbo.visit($(this).data("href"));
});
}

Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/channels/pg_matching_channel.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function () {
$(document).on('turbo-migration:load', function () {

if ($.isController('programming_groups') && window.location.pathname.includes('programming_groups/new')) {
const matching_page = $('#matching');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function () {
$(document).on('turbo-migration:load', function () {

if (window.location.pathname.includes('/implement')) {
var editor = $('#editor');
Expand Down
3 changes: 1 addition & 2 deletions app/assets/javascripts/codeharbor_link.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {
if($.isController('codeharbor_links')) {
if ($('.edit_codeharbor_link, .new_codeharbor_link').isPresent()) {

Expand Down Expand Up @@ -33,4 +33,3 @@ $(document).on('turbolinks:load', function() {
}
}
});

21 changes: 15 additions & 6 deletions app/assets/javascripts/community_solution.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {

if ($.isController('community_solutions') && $('#community-solution-editor').isPresent()) {
CodeOceanEditor.sendEvents = false;
Expand All @@ -25,6 +25,18 @@ $(document).on('turbolinks:load', function() {
}
});

$(document).one('turbo-migration:load', function() {
if ($.isController('community_solutions') && $('#community-solution-editor').isPresent()) {
$(document).one('turbo:visit', unloadEditorHandler);
$(window).one('beforeunload', unloadEditorHandler);
}
});

function unloadEditorHandler() {
CodeOceanEditor.autosaveIfChanged();
CodeOceanEditor.unloadEditor();
}

function submitCode(event) {
const button = $(event.target) || $('#submit');
this.newSentryTransaction(button, async () => {
Expand All @@ -35,10 +47,7 @@ function submitCode(event) {
if (!submission) return;
if (!submission.redirect) return;

this.autosaveIfChanged();
await this.stopCode(event);
this.editors = [];
Turbolinks.clearCache();
Turbolinks.visit(submission.redirect);
unloadEditorHandler();
Turbo.visit(submission.redirect);
});
}
2 changes: 1 addition & 1 deletion app/assets/javascripts/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {
var CHART_START = window.vis ? vis.moment().add(-1, 'minute') : undefined;
var DEFAULT_REFRESH_INTERVAL = 5000;

Expand Down
18 changes: 9 additions & 9 deletions app/assets/javascripts/editor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function(event) {
$(document).on('turbo-migration:load', function(event) {

//Merge all editor components.
$.extend(
Expand All @@ -13,16 +13,16 @@ $(document).on('turbolinks:load', function(event) {
CodeOceanEditorRequestForComments
);

if ($('#editor').isPresent() && CodeOceanEditor && event.originalEvent.data.url.includes("/implement")) {
if ($('#editor').isPresent() && CodeOceanEditor && event.detail.url.includes("/implement")) {
CodeOceanEditor.initializeEverything();
}
});

function handleThemeChangeEvent(event) {
if (CodeOceanEditor) {
CodeOceanEditor.THEME = event.detail.currentTheme === 'dark' ? 'ace/theme/tomorrow_night' : 'ace/theme/tomorrow';
document.dispatchEvent(new Event('theme:change:ace'));
}
function handleThemeChangeEvent(event) {
if (CodeOceanEditor) {
CodeOceanEditor.THEME = event.detail.currentTheme === 'dark' ? 'ace/theme/tomorrow_night' : 'ace/theme/tomorrow';
document.dispatchEvent(new Event('theme:change:ace'));
}
}

$(document).on('theme:change', handleThemeChangeEvent.bind(this));
});
$(document).on('theme:change', handleThemeChangeEvent);
71 changes: 58 additions & 13 deletions app/assets/javascripts/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ var CodeOceanEditor = {
}

const bottom = considerStatusbar ? ($('#statusbar').height() || 0) : 0;
// calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins
return window.innerHeight - jqueryElement.offset().top - bottom - 5;
// calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 7 for bar margins
return window.innerHeight - jqueryElement.offset().top - bottom - 7;
},

resizeParentOfAceEditor: function (element) {
Expand Down Expand Up @@ -418,15 +418,39 @@ var CodeOceanEditor = {
this.showFrame(frame);
this.toggleButtonStates();
}.bind(this));
$(document).on('theme:change', function(event) {

this.installFileTreeEventHandlers(filesInstance);
},

installFileTreeEventHandlers: function (filesInstance) {
// Prevent duplicate event listeners by removing them during unload.
const themeListener = this.createFileTreeThemeChangeListener(filesInstance);
const jsTree = filesInstance?.jstree(true);
$(document).on('theme:change', themeListener);
$(document).one('turbo:visit', function() {
$(document).off('theme:change', themeListener);
if (jsTree && jsTree.element) {
jsTree.destroy(true);
}
});
$(window).one('beforeunload', function() {
$(document).off('theme:change', themeListener);
if (jsTree && jsTree.element) {
jsTree.destroy(true);
}
});
},

createFileTreeThemeChangeListener: function (filesInstance) {
return function (event) {
const jsTree = filesInstance?.jstree(true);

if (jsTree) {
const newColorScheme = event.detail.currentTheme;
// Update the JStree theme
jsTree?.set_theme(newColorScheme === "dark" ? "default-dark" : "default");
}
});
}
},

initializeFileTreeButtons: function () {
Expand Down Expand Up @@ -935,10 +959,6 @@ var CodeOceanEditor = {
$('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed');
},

initializeSideBarTooltips: function () {
$('[data-bs-toggle="tooltip"]').tooltip()
},

initializeDescriptionToggle: function () {
$('#exercise-headline').on('click', this.toggleDescriptionCard.bind(this));
$('a#toggle').on('click', this.toggleDescriptionCard.bind(this));
Expand Down Expand Up @@ -1096,7 +1116,6 @@ var CodeOceanEditor = {
this.initializeSideBarCollapse();
this.initializeOutputBarToggle();
this.initializeDescriptionToggle();
this.initializeSideBarTooltips();
this.initializeInterventionTimer();
this.initPrompt();
this.renderScore();
Expand All @@ -1106,12 +1125,38 @@ var CodeOceanEditor = {
this.initializeDeadlines();
CodeOceanEditorTips.initializeEventHandlers();

window.addEventListener("turbolinks:before-render", App.synchronized_editor?.disconnect.bind(App.synchronized_editor));
window.addEventListener("beforeunload", App.synchronized_editor?.disconnect.bind(App.synchronized_editor));
$(document).one("turbo:visit", this.unloadEverything.bind(this, App.synchronized_editor));
$(window).one("beforeunload", this.unloadEverything.bind(this, App.synchronized_editor));

window.addEventListener("turbolinks:before-render", this.autosaveIfChanged.bind(this));
window.addEventListener("beforeunload", this.autosaveIfChanged.bind(this));
// create autosave when the editor is opened the first time
this.autosave();
},

unloadEverything: function () {
App.synchronized_editor?.disconnect();
this.autosaveIfChanged();
this.unloadEditor();
this.teardownEventHandlers();
},

unloadEditor: function () {
$(document).off('theme:change:ace');
CodeOceanEditor.cacheEditorContent();
CodeOceanEditor.destroyEditors();
},

cacheEditorContent: function () {
// Persist the content of the editors in a hidden textarea to enable Turbo caching.
// In this case, we iterate over _all_ editors, not just writable ones.
for (const [file_id, editor] of this.editor_for_file) {
const file_content = editor.getValue();
const editorContent = $(`.editor-content[data-file-id='${file_id}']`);
editorContent.text(file_content);
}
},

destroyEditors: function () {
CodeOceanEditor.editors.forEach(editor => editor.destroy());
CodeOceanEditor.editors = [];
}
};
2 changes: 1 addition & 1 deletion app/assets/javascripts/error_templates.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {
if ($.isController('error_templates')) {
const button = $('#add-attribute').find('button')
button.on('click', function () {
Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/exercise_collections.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {
if ($.isController('exercise_collections')) {
var dataElement = $('#data');
var exerciseList = $('#exercise-list');
Expand Down Expand Up @@ -100,7 +100,7 @@ $(document).on('turbolinks:load', function() {
tooltip.style("display", "none");
})
.on("click", function (_event, d) {
Turbolinks.visit(Routes.statistics_exercise_path(d.exercise_id));
Turbo.visit(Routes.statistics_exercise_path(d.exercise_id));
})
.attr("x", function (d) {
return x(d.index);
Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/exercise_graphs.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$(document).on('turbolinks:load', function() {
$(document).on('turbo-migration:load', function() {
// /exercises/38/statistics good for testing

if ($.isController('exercises') && $('.graph-functions-2').isPresent()) {
Expand Down
Loading