Skip to content

Commit

Permalink
Merge pull request #1743 from unboxed/sortable-js
Browse files Browse the repository at this point in the history
Allow drag and drop sorting
  • Loading branch information
benbaumann95 authored Apr 25, 2024
2 parents 97eb1e1 + fcafd9a commit fe8e61f
Show file tree
Hide file tree
Showing 17 changed files with 268 additions and 19 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ gem "dotenv-rails", require: "dotenv/rails-now"

gem "aasm"
gem "activerecord-postgis-adapter"
gem "acts_as_list"
gem "appsignal"
gem "aws-sdk-s3", require: false
gem "bootsnap", ">= 1.4.2", require: false
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ GEM
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
acts_as_list (1.1.0)
activerecord (>= 4.2)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
appsignal (3.4.5)
Expand Down Expand Up @@ -639,6 +641,7 @@ PLATFORMS
DEPENDENCIES
aasm
activerecord-postgis-adapter
acts_as_list
appsignal
aws-sdk-s3
bootsnap (>= 1.4.2)
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/_main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ $govuk-page-width: 1100px;
@import "components/inset_text";
@import "components/loading_spinner";
@import "components/primary_navigation";
@import "components/sortable";
@import "components/table";
@import "components/task_list";
@import "consultation";
Expand Down
3 changes: 3 additions & 0 deletions app/assets/stylesheets/components/_sortable.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.sortable-list {
cursor: move;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module PlanningApplications
module Assessment
module Informatives
class PositionsController < AuthenticationController
before_action :set_planning_application
before_action :set_informative_set
before_action :set_informative

def update
if @informative.insert_at(informative_position)
head :no_content
else
render json: @informative.errors, status: :unprocessable_entity
end
end

private

def set_informative_set
@informative_set = @planning_application.informative_set
end

def set_informative
@informative = @informative_set.informatives.find(params[:informative_id])
end

def informative_position_params
params.require(:informative).permit(:position)
end

def informative_position
Integer(informative_position_params[:position])
end
end
end
end
end
3 changes: 3 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@ application.register("submit-form", SubmitFormController)

import UnsavedChangesController from "./unsaved_changes_controller.js"
application.register("unsaved-changes", UnsavedChangesController)

import Sortable from "./sortable_controller.js"
application.register("sortable", Sortable)
57 changes: 57 additions & 0 deletions app/javascript/controllers/sortable_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Controller } from "@hotwired/stimulus"
import { put } from "@rails/request.js"
import Sortable from "sortablejs"

export default class extends Controller {
static values = { url: String }

connect() {
this.initializeSortable()
}

disconnect() {
this.sortable.destroy()
}

initializeSortable() {
this.sortable = Sortable.create(this.element, {
animation: 350,
handle: "[data-sortable-handle]",
onEnd: this.onEnd.bind(this),
})
}

onEnd(event) {
const { newIndex, item } = event
const url = item.dataset.sortableUrl

put(url, {
body: JSON.stringify({
informative: {
position: newIndex,
},
}),
})
.then(() => {
const modelName = item.dataset.modelName

if (modelName) {
this.updatePositions(modelName)
}
})
.catch((error) => {
console.error("Error updating position:", error)
})
}

updatePositions(modelName) {
const items = this.element.querySelectorAll("li")

items.forEach((item, index) => {
const positionElement = item.querySelector(".govuk-hint")
if (positionElement) {
positionElement.textContent = `${modelName} ${index + 1}`
}
})
}
}
5 changes: 2 additions & 3 deletions app/models/informative.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

class Informative < ApplicationRecord
belongs_to :informative_set
acts_as_list scope: :informative_set

validates :title, :text, presence: true

validates :title, :text, uniqueness: {scope: :informative_set_id}
validates :title, :text, presence: true, uniqueness: {scope: :informative_set_id}
end
2 changes: 1 addition & 1 deletion app/models/informative_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class InformativeSet < ApplicationRecord
belongs_to :planning_application
has_many :reviews, as: :owner, dependent: :destroy, class_name: "Review"
has_many :informatives, dependent: :destroy
has_many :informatives, -> { order(position: :asc) }, dependent: :destroy

accepts_nested_attributes_for :reviews

Expand Down
2 changes: 1 addition & 1 deletion app/views/planning_applications/_decision_notice.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
<h3 class="govuk-heading-s">
Informatives:
</h3>
<ol class="govuk-list govuk-list--number">
<ol class="govuk-list govuk-list--number" id="informatives-list">
<% @planning_application.informative_set.informatives.each do |informative| %>
<li>
<%= informative.title %><br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,26 @@
<div class="govuk-grid-row" data-controller="informatives">
<div class="govuk-grid-column-two-thirds">
<% if @informative_set.informatives.select(&:persisted?).any? %>
<% @informative_set.informatives.select(&:persisted?).each_with_index do |informative, index| %>
<ul data-controller="sortable" class="govuk-list">
<hr class="govuk-section-break govuk-section-break--l govuk-section-break--visible">
<span class="govuk-hint">
Informative <%= index +1 %>
</span>
<h2 class="govuk-heading-m">
<%= informative.title %>
</h2>
<%= render(FormattedContentComponent.new(text: informative.text)) %>
<%= govuk_link_to "Edit", edit_planning_application_assessment_informative_path(@planning_application, informative) %>
<%= govuk_link_to "Remove", planning_application_assessment_informative_path(@planning_application, informative), method: :delete %>
<% end %>
<hr class="govuk-section-break govuk-section-break--l govuk-section-break--visible">
<% @informative_set.informatives.select(&:persisted?).each do |informative| %>
<%= content_tag :li,
class: "sortable-list",
id: dom_id(informative),
data: {
model_name: informative.class.name,
sortable_url: planning_application_assessment_informative_positions_path(@planning_application, informative),
sortable_handle: true
} do %>
<span class="govuk-hint">Informative <%= informative.position %></span>
<h2 class="govuk-heading-m"><%= informative.title %></h2>
<%= render(FormattedContentComponent.new(text: informative.text)) %>
<%= govuk_link_to "Edit", edit_planning_application_assessment_informative_path(@planning_application, informative) %>
<%= govuk_link_to "Remove", planning_application_assessment_informative_path(@planning_application, informative), method: :delete %>
<hr class="govuk-section-break govuk-section-break--l govuk-section-break--visible">
<% end %>
<% end %>
</ul>
<% else %>
<p class="govuk-body"><strong>No informatives added yet</strong></p>
<% end %>
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@

resources :informatives, except: %i[show] do
post :complete, on: :collection
resource :positions, only: %i[update], module: :informatives
end
end

Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20240424142947_add_position_to_informative.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddPositionToInformative < ActiveRecord::Migration[7.1]
def change
add_column :informatives, :position, :integer
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2024_04_22_112221) do
ActiveRecord::Schema[7.1].define(version: 2024_04_24_142947) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "postgis"
Expand Down Expand Up @@ -400,6 +400,7 @@
t.bigint "informative_set_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "position"
t.index ["informative_set_id"], name: "ix_informatives_on_informative_set_id"
t.index ["text", "informative_set_id"], name: "ix_informatives_on_text__informative_set_id", unique: true
t.index ["title", "informative_set_id"], name: "ix_informatives_on_title__informative_set_id", unique: true
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
"@hotwired/stimulus": "^3.2.1",
"@opensystemslab/map": "^0.8.0",
"@rails/activestorage": "^7.0.4-2",
"@rails/request.js": "^0.0.9",
"@rails/ujs": "^7.0.4-2",
"@stimulus-components/sortable": "^5.0.1",
"accessible-autocomplete": "^2.0.4",
"esbuild": "^0.19.2",
"govuk-frontend": "^5.2.0",
"is-svg": "^5.0.0",
"jspdf": "^2.5.1",
"puppeteer": "^21.0.0",
"sortablejs": "^1.15.2",
"stimulus": "^1.1.1",
"toastr": "^2.1.4"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
click_link "Add informatives"
end

expect(page).to have_content "Add informatives"
expect(page).to have_selector("h1", text: "Add informatives")
expect(page).to have_content "No informatives added yet"

fill_in "Start typing to choose an informative from a list", with: "Section"
Expand Down Expand Up @@ -161,6 +161,115 @@
expect(page).to have_content "No informatives added yet"
end

context "when changing the list position" do
let(:informative_set) { planning_application.informative_set }
let!(:informative_one) { create(:informative, informative_set:, title: "Title 1", text: "Text 1", position: 1) }
let!(:informative_two) { create(:informative, informative_set:, title: "Title 2", text: "Text 2", position: 2) }
let!(:informative_three) { create(:informative, informative_set:, title: "Title 3", text: "Text 3", position: 3) }

it "I can drag and drop to sort the informatives" do
click_link "Add informatives"

informative_one_handle = find("li.sortable-list", text: "Title 1")
informative_two_handle = find("li.sortable-list", text: "Title 2")
informative_three_handle = find("li.sortable-list", text: "Title 3")

within("li.sortable-list:nth-of-type(1)") do
expect(page).to have_selector("span", text: "Informative 1")
expect(page).to have_selector("h2", text: "Title 1")
end
within("li.sortable-list:nth-of-type(2)") do
expect(page).to have_selector("span", text: "Informative 2")
expect(page).to have_selector("h2", text: "Title 2")
end
within("li.sortable-list:nth-of-type(3)") do
expect(page).to have_selector("span", text: "Informative 3")
expect(page).to have_selector("h2", text: "Title 3")
end

informative_one_handle.drag_to(informative_two_handle)

within("li.sortable-list:nth-of-type(1)") do
expect(page).to have_selector("span", text: "Informative 1")
expect(page).to have_selector("h2", text: "Title 2")
end
within("li.sortable-list:nth-of-type(2)") do
expect(page).to have_selector("span", text: "Informative 2")
expect(page).to have_selector("h2", text: "Title 1")
end
within("li.sortable-list:nth-of-type(3)") do
expect(page).to have_selector("span", text: "Informative 3")
expect(page).to have_selector("h2", text: "Title 3")
end
expect(informative_one.reload.position).to eq(2)
expect(informative_two.reload.position).to eq(1)
expect(informative_three.reload.position).to eq(3)

informative_one_handle.drag_to(informative_three_handle)

within("li.sortable-list:nth-of-type(1)") do
expect(page).to have_selector("span", text: "Informative 1")
expect(page).to have_selector("h2", text: "Title 2")
end
within("li.sortable-list:nth-of-type(2)") do
expect(page).to have_selector("span", text: "Informative 2")
expect(page).to have_selector("h2", text: "Title 3")
end
within("li.sortable-list:nth-of-type(3)") do
expect(page).to have_selector("span", text: "Informative 3")
expect(page).to have_selector("h2", text: "Title 1")
end
expect(informative_one.reload.position).to eq(3)
expect(informative_two.reload.position).to eq(1)
expect(informative_three.reload.position).to eq(2)

informative_three_handle.drag_to(informative_two_handle)

within("li.sortable-list:nth-of-type(1)") do
expect(page).to have_selector("span", text: "Informative 1")
expect(page).to have_selector("h2", text: "Title 3")
end
within("li.sortable-list:nth-of-type(2)") do
expect(page).to have_selector("span", text: "Informative 2")
expect(page).to have_selector("h2", text: "Title 2")
end
within("li.sortable-list:nth-of-type(3)") do
expect(page).to have_selector("span", text: "Informative 3")
expect(page).to have_selector("h2", text: "Title 1")
end
expect(informative_one.reload.position).to eq(3)
expect(informative_two.reload.position).to eq(2)
expect(informative_three.reload.position).to eq(1)

click_link "Back"
click_link "Add informatives"

within("li.sortable-list:nth-of-type(1)") do
expect(page).to have_selector("span", text: "Informative 1")
expect(page).to have_selector("h2", text: "Title 3")
end
within("li.sortable-list:nth-of-type(2)") do
expect(page).to have_selector("span", text: "Informative 2")
expect(page).to have_selector("h2", text: "Title 2")
end
within("li.sortable-list:nth-of-type(3)") do
expect(page).to have_selector("span", text: "Informative 3")
expect(page).to have_selector("h2", text: "Title 1")
end

# Check the correct order on decision notice
create(:recommendation, :assessment_in_progress, planning_application:)
click_link "Back"
click_link "Review and submit recommendation"

within("#informatives-list") do
expect(page).to have_selector("li:nth-of-type(1)", text: "Title 3")
expect(page).to have_selector("li:nth-of-type(2)", text: "Title 2")
expect(page).to have_selector("li:nth-of-type(3)", text: "Title 1")
end
end
end

it "shows errors" do
click_link "Add informatives"

Expand Down
Loading

0 comments on commit fe8e61f

Please sign in to comment.