From 73e885c12eb94ab2e514b49270e3b59204bf90d0 Mon Sep 17 00:00:00 2001 From: Shelley Nason Date: Thu, 19 Feb 2026 14:17:57 -0500 Subject: [PATCH] Fix drag-and-drop question reordering. (#1977) --- .../components/forms/edit/_builder.html.erb | 71 ++++++++++--------- spec/features/admin/forms_spec.rb | 26 +++++++ 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/app/views/components/forms/edit/_builder.html.erb b/app/views/components/forms/edit/_builder.html.erb index 33deae5de..8bb4b5386 100644 --- a/app/views/components/forms/edit/_builder.html.erb +++ b/app/views/components/forms/edit/_builder.html.erb @@ -196,6 +196,7 @@ $(function() { url: $(this).attr("href"), success: function (data) { $(".sorting-div").append(data); + initializeSortableQuestions($(".questions").last()); } }); }); @@ -264,41 +265,9 @@ $(function() { }); }); - // Initialize sortable for questions with better error handling + // Initialize sortable for questions try { - $(".questions").sortable({ - items: '.question', - connectWith: ".questions", - handle: ".drag-handle", - distance: 20, - helper: 'clone', - tolerance: 'pointer', - update: function(e, ui) { - try { - var section_id = $(this).closest(".form-section-div").attr('data-id'); - var data = $(this).sortable('serialize'); - if (!data) { - console.error("Failed to serialize sortable data"); - return; - } - data = data + "&form_section_id=" + section_id; - $.ajax({ - url: '<%= sort_questions_admin_form_questions_path(@form) %>', - type: "PATCH", - - error: function(xhr, status, error) { - console.error("Sort update failed:", error); - ui.item.animate({ left: 0 }, 300); // Visual feedback on failure - } - }); - } catch (err) { - console.error("Error during sort update:", err); - } - }, - start: function(e, ui) { - ui.placeholder.height(ui.item.height()); - } - }).disableSelection(); + initializeSortableQuestions($(".questions")); } catch (err) { console.error("Failed to initialize question sortable:", err); } @@ -442,6 +411,40 @@ function textCounter(field,maxlimit) { countfield.innerText = "" + (maxlimit - field.value.length) + " <%= t :characters_left %>"; } } +function initializeSortableQuestions($questionContainer) { + $questionContainer.sortable({ + items: '.question', + handle: ".drag-handle", + distance: 20, + helper: 'clone', + tolerance: 'pointer', + update: function(e, ui) { + try { + var section_id = $(this).closest(".form-section-div").attr('data-id'); + var data = $(this).sortable('serialize'); + if (!data) { + console.error("Failed to serialize sortable data"); + return; + } + data = data + "&form_section_id=" + section_id; + $.ajax({ + url: '<%= sort_questions_admin_form_questions_path(@form) %>', + type: "PATCH", + data: data, + error: () => { + alert("Failed to update question order"); + $(this).sortable("cancel"); + } + }); + } catch (err) { + console.error("Error during sort update:", err); + } + }, + start: function(e, ui) { + ui.placeholder.height(ui.item.height()); + } + }).disableSelection(); +} document.addEventListener("DOMContentLoaded", function () { document.querySelectorAll(".quill").forEach((wrapper) => { diff --git a/spec/features/admin/forms_spec.rb b/spec/features/admin/forms_spec.rb index 9eb375190..c4d22280c 100644 --- a/spec/features/admin/forms_spec.rb +++ b/spec/features/admin/forms_spec.rb @@ -1284,6 +1284,32 @@ end end + describe 'reordering Questions' do + let!(:question1) { FactoryBot.create(:question, text: 'Question 1', answer_field: :answer_01, form:, form_section: form.form_sections.first, position: 1) } + let!(:question2) { FactoryBot.create(:question, text: 'Question 2', answer_field: :answer_02, form:, form_section: form.form_sections.first, position: 2) } + + before do + visit questions_admin_form_path(form) + wait_for_builder + source = find('.question', text: 'Question 1').find('.drag-handle') + target = find('.question', text: 'Question 2') + source.drag_to(target) + wait_for_ajax + end + + it 'moves the first question down' do + expect(page.all('.question')[0]).to have_content('Question 2') + expect(page.all('.question')[1]).to have_content('Question 1') + end + + it 'persists after refresh' do + visit questions_admin_form_path(form) + wait_for_builder + expect(page.all('.question')[0]).to have_content('Question 2') + expect(page.all('.question')[1]).to have_content('Question 1') + end + end + describe 'adding Question Options' do describe 'add Radio Button options' do let!(:radio_button_question) { FactoryBot.create(:question, :with_radio_buttons, form:, form_section: form.form_sections.first) }