From ee5dbcf33e3a5faf9f1569d8aa4ed8de8cdf701f Mon Sep 17 00:00:00 2001
From: david
Date: Mon, 11 Nov 2024 04:04:13 +0100
Subject: [PATCH] Make stuff sortable
---
app/controllers/elements_controller.rb | 9 ++-
app/controllers/pages_controller.rb | 7 ++-
.../success_criteria_controller.rb | 2 +-
app/javascript/controllers/index.js | 6 ++
.../controllers/sort_elements_controller.js | 7 +++
.../controllers/sortable_controller.js | 55 +++++++++++++++++++
app/models/checklist_entry.rb | 1 +
app/models/element.rb | 15 ++++-
app/models/page.rb | 11 +++-
app/models/report.rb | 2 +-
app/models/success_criterion.rb | 12 ++++
app/views/elements/_element.html.erb | 2 +-
app/views/elements/_page_nav_row.html.slim | 8 ++-
app/views/elements/update.turbo_stream.slim | 7 +++
app/views/pages/_page.html.erb | 8 +--
app/views/pages/update.turbo_stream.slim | 7 +++
app/views/reports/_page_nav.html.slim | 11 ++--
.../_success_criterion.html.slim | 2 +-
.../success_criteria/update.turbo_stream.slim | 5 +-
.../20241111011637_remove_position_indices.rb | 7 +++
db/schema.rb | 5 +-
21 files changed, 161 insertions(+), 28 deletions(-)
create mode 100644 app/javascript/controllers/sort_elements_controller.js
create mode 100644 app/javascript/controllers/sortable_controller.js
create mode 100644 app/views/elements/update.turbo_stream.slim
create mode 100644 app/views/pages/update.turbo_stream.slim
create mode 100644 db/migrate/20241111011637_remove_position_indices.rb
diff --git a/app/controllers/elements_controller.rb b/app/controllers/elements_controller.rb
index 109b1cd..152c904 100644
--- a/app/controllers/elements_controller.rb
+++ b/app/controllers/elements_controller.rb
@@ -49,7 +49,12 @@ class ElementsController < ApplicationController
# PATCH/PUT /elements/1
def update
if @element.update(element_params)
- redirect_to @element, notice: "Element was successfully updated.", status: :see_other
+ respond_to do |format|
+ format.turbo_stream
+ format.html do
+ redirect_to @element, notice: "Element was successfully updated.", status: :see_other
+ end
+ end
else
render :edit, status: :unprocessable_entity
end
@@ -82,6 +87,6 @@ class ElementsController < ApplicationController
# Only allow a list of trusted parameters through.
def element_params
- params.require(:element).permit(:page_id, :title, :description)
+ params.require(:element).permit(:page_id, :title, :description, :position)
end
end
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb
index 2352858..13f2fb8 100644
--- a/app/controllers/pages_controller.rb
+++ b/app/controllers/pages_controller.rb
@@ -35,7 +35,12 @@ class PagesController < ApplicationController
# PATCH/PUT /pages/1
def update
if @page.update(page_params)
- redirect_to @page, notice: "Page was successfully updated.", status: :see_other
+ respond_to do |format|
+ format.turbo_stream
+ format.html do
+ redirect_to @page, notice: "Page was successfully updated.", status: :see_other
+ end
+ end
else
render :edit, status: :unprocessable_entity
end
diff --git a/app/controllers/success_criteria_controller.rb b/app/controllers/success_criteria_controller.rb
index d22c7b9..4ae7840 100644
--- a/app/controllers/success_criteria_controller.rb
+++ b/app/controllers/success_criteria_controller.rb
@@ -115,7 +115,7 @@ class SuccessCriteriaController < ApplicationController
# Only allow a list of trusted parameters through.
def success_criterion_params
- params.require(:success_criterion).permit(:element_id, :title, :quick_criterion, :quick_fail, :quick_fix, :priority, :level, :result, :test_comment, :check_id)
+ params.require(:success_criterion).permit(:element_id, :title, :quick_criterion, :quick_fail, :quick_fix, :priority, :level, :result, :test_comment, :check_id, :position)
end
def set_element
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
index 1274033..6803226 100644
--- a/app/javascript/controllers/index.js
+++ b/app/javascript/controllers/index.js
@@ -34,6 +34,12 @@ application.register("rich-text-link-targets", RichTextLinkTargetsController)
import SetThemeController from "./set_theme_controller"
application.register("set-theme", SetThemeController)
+import SortElementsController from "./sort_elements_controller"
+application.register("sort-elements", SortElementsController)
+
+import SortableController from "./sortable_controller"
+application.register("sortable", SortableController)
+
import ThemeSwitcherController from "./theme_switcher_controller"
application.register("theme-switcher", ThemeSwitcherController)
diff --git a/app/javascript/controllers/sort_elements_controller.js b/app/javascript/controllers/sort_elements_controller.js
new file mode 100644
index 0000000..4a0fb43
--- /dev/null
+++ b/app/javascript/controllers/sort_elements_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from "@hotwired/stimulus"
+
+// Connects to data-controller="sort-elements"
+export default class extends Controller {
+ connect() {
+ }
+}
diff --git a/app/javascript/controllers/sortable_controller.js b/app/javascript/controllers/sortable_controller.js
new file mode 100644
index 0000000..a5b9ca3
--- /dev/null
+++ b/app/javascript/controllers/sortable_controller.js
@@ -0,0 +1,55 @@
+import { Controller } from "@hotwired/stimulus"
+import Sortable from "sortablejs"
+import { put } from "@rails/request.js";
+
+// Connects to data-controller="sortable"
+export default class extends Controller {
+ linkedElement = null
+ connect() {
+ this.element.style.cursor = "grab"
+
+ console.log("dataset", this.element.dataset)
+ if (this.element.dataset["linkedElementId"]) {
+ this.linkedElement = document.getElementById(this.element.dataset["linkedElementId"])
+ console.log("Has a linked element", this.linkedElement)
+ }
+
+ new Sortable(this.element, {
+ group: this.groupValue,
+ onEnd: this.onEndFactory(this.linkedElement),
+ })
+ }
+
+ onEndFactory(linkedElement) {
+ return function (event) {
+ const position = event.newIndex + 1
+ const url = event.item.dataset["sortableUrl"]
+ const formName = event.item.dataset["formName"]
+ const positionAttribute = event.item.dataset["positionAttribute"]
+ let body = {}
+ body[formName] = {}
+ body[formName][positionAttribute] = position
+ console.log("event", event, "url", url)
+ // Expect backend to update list items via turbo if necessary
+ put(url, {
+ body: JSON.stringify(body),
+ contentType: "application/json",
+ headers: {
+ "Accept": "text/vnd.turbo-stream.html, text/html, application/xhtml+xml"
+ }
+ })
+ console.log(linkedElement)
+ if (linkedElement) {
+ console.log("move linked", linkedElement)
+ let children = linkedElement.children
+ let child = children[event.oldIndex]
+ let newAfter = children[event.newIndex]
+ if (event.oldIndex < event.newIndex) {
+ newAfter = children[event.newIndex + 1]
+ }
+ console.log("move ", child, "before", newAfter)
+ child.parentNode.insertBefore(child, newAfter)
+ }
+ }
+ }
+}
diff --git a/app/models/checklist_entry.rb b/app/models/checklist_entry.rb
index 2cfe0c6..1d74b64 100644
--- a/app/models/checklist_entry.rb
+++ b/app/models/checklist_entry.rb
@@ -7,6 +7,7 @@ class ChecklistEntry < ApplicationRecord
before_validation :set_position
before_update :update_positions, if: :position_changed?
+ private
def set_position
self.position ||= (checklist.checklist_entries.pluck(:position).max || 0) + 1
end
diff --git a/app/models/element.rb b/app/models/element.rb
index 5d86afe..11f4b49 100644
--- a/app/models/element.rb
+++ b/app/models/element.rb
@@ -8,7 +8,8 @@ class Element < ApplicationRecord
delegate :report, to: :page
- after_validation :set_position
+ before_validation :set_position
+ before_update :update_positions, if: :position_changed?
# Calculate actual conformity level:
# - if a success_criterion has result :failed -> the confirmity_level
@@ -33,12 +34,20 @@ class Element < ApplicationRecord
@max_level ||= success_criteria.reject(&:not_applicable?).max(&:level)
end
+ def number
+ "#{page.position}.#{position}"
+ end
+
+ private
def set_position
Rails.logger.debug("element: position #{position}")
self.position ||= (page.elements.pluck(:position).max || 0) + 1
end
- def number
- "#{page.position}.#{position}"
+ def update_positions
+ if position_was
+ page.elements.where("position > ?", position_was).update_all("position = position - 1")
+ end
+ page.elements.where(position: position..).update_all("position = position + 1")
end
end
diff --git a/app/models/page.rb b/app/models/page.rb
index e19ab6f..29bb751 100644
--- a/app/models/page.rb
+++ b/app/models/page.rb
@@ -1,13 +1,22 @@
class Page < ApplicationRecord
belongs_to :report, touch: true
- has_many :elements, dependent: :destroy
+ has_many :elements, -> { order(:position) }, dependent: :destroy
has_rich_text :comment
before_validation :set_position
+ before_update :update_positions, if: :position_changed?
+
private
def set_position
self.position ||= (report.pages.pluck(:position).max || 0) + 1
end
+
+ def update_positions
+ if position_was
+ report.pages.where("position > ?", position_was).update_all("position = position - 1")
+ end
+ report.pages.where(position: position..).update_all("position = position + 1")
+ end
end
diff --git a/app/models/report.rb b/app/models/report.rb
index 9acc234..cb36945 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Report < ApplicationRecord
- has_many :pages, dependent: :destroy
+ has_many :pages, -> { order(:position) }, dependent: :destroy
has_many :elements, through: :pages, dependent: :destroy
has_rich_text :comment
diff --git a/app/models/success_criterion.rb b/app/models/success_criterion.rb
index 218bb49..03c97fa 100644
--- a/app/models/success_criterion.rb
+++ b/app/models/success_criterion.rb
@@ -15,6 +15,7 @@ class SuccessCriterion < ApplicationRecord
enum :level, %i[A AA AAA]
before_save :set_position
+ before_update :update_positions, if: :position_changed?
def level_value
return nil unless level
@@ -26,9 +27,20 @@ class SuccessCriterion < ApplicationRecord
"HEADER"
end
+ def number
+ [ page.position, element.position, position ].join(".")
+ end
+
private
def set_position
self.position ||= (element.success_criteria.pluck(:position).max || 0) + 1
Rails.logger.debug("set position: "+position.to_s)
end
+
+ def update_positions
+ if position_was
+ element.success_criteria.where("position > ?", position_was).update_all("position = position - 1")
+ end
+ element.success_criteria.where(position: position..).update_all("position = position + 1")
+ end
end
diff --git a/app/views/elements/_element.html.erb b/app/views/elements/_element.html.erb
index 83902bc..7cd7df2 100644
--- a/app/views/elements/_element.html.erb
+++ b/app/views/elements/_element.html.erb
@@ -26,7 +26,7 @@
<% end %>
-
+
<% element.success_criteria.each do |sc| %>
<%= render sc %>
<% end %>
diff --git a/app/views/elements/_page_nav_row.html.slim b/app/views/elements/_page_nav_row.html.slim
index 3e7fe42..70f8abf 100644
--- a/app/views/elements/_page_nav_row.html.slim
+++ b/app/views/elements/_page_nav_row.html.slim
@@ -1,9 +1,11 @@
-li id=dom_id(element, :page_nav_row)
+li id=dom_id(element, :page_nav_row) data={ "sortable-url": element_path(element), "form-name": "element", "position-attribute": "position" }
- if current_page
=< link_to("##{dom_id(element)}", data: { "turbo": false }) do
i.bi.bi-boxes.me-1
- =< "#{element.number} #{element.title}"
+ span id=dom_id(element, :page_nav_title)
+ = "#{element.number} #{element.title}"
- else
=< link_to(report_path(element.report, page_id: element.page.id, anchor: dom_id(element)), data: { "turbo": false }) do
i.bi.bi-boxes.me-1
- =< "#{element.number} #{element.title}"
\ No newline at end of file
+ span id=dom_id(element, :page_nav_title)
+ =< "#{element.number} #{element.title}"
\ No newline at end of file
diff --git a/app/views/elements/update.turbo_stream.slim b/app/views/elements/update.turbo_stream.slim
new file mode 100644
index 0000000..e6b478c
--- /dev/null
+++ b/app/views/elements/update.turbo_stream.slim
@@ -0,0 +1,7 @@
+- @element.page.elements.each do |element|
+ = turbo_stream.update dom_id(element, :page_nav_title), "#{element.number} #{element.title}"
+ = turbo_stream.update dom_id(element, :title), "#{element.page.position}.#{element.position} #{element.title}"
+ / - element.success_criteria.each do |sc|
+ / = turbo_stream.update dom_id(sc, :title), "#{sc.page.position}.#{sc.element.position}.#{sc.position} #{sc.title}"
+ - element.success_criteria.each do |sc|
+ = turbo_stream.update dom_id(sc, :position), "#{sc.page.position}.#{sc.element.position}.#{sc.position}"
\ No newline at end of file
diff --git a/app/views/pages/_page.html.erb b/app/views/pages/_page.html.erb
index 4ffc616..3c5ef30 100644
--- a/app/views/pages/_page.html.erb
+++ b/app/views/pages/_page.html.erb
@@ -1,9 +1,9 @@
+
-
<% page.elements.each do |element| %>
<%= render element %>
<% end %>
diff --git a/app/views/pages/update.turbo_stream.slim b/app/views/pages/update.turbo_stream.slim
new file mode 100644
index 0000000..298fcff
--- /dev/null
+++ b/app/views/pages/update.turbo_stream.slim
@@ -0,0 +1,7 @@
+- @page.report.pages.each do |page|
+ = turbo_stream.update dom_id(page, :title), "#{page.position} #{page.path}"
+ - page.elements.each do |element|
+ = turbo_stream.update dom_id(element, :title), "#{element.page.position}.#{element.position} #{element.title}"
+ = turbo_stream.replace dom_id(element, :page_nav_row), partial: "elements/page_nav_row", locals: { element: element, current_page: element.page == @page}
+ - element.success_criteria.each do |sc|
+ = turbo_stream.update dom_id(sc, :position), "#{sc.page.position}.#{sc.element.position}.#{sc.position}"
\ No newline at end of file
diff --git a/app/views/reports/_page_nav.html.slim b/app/views/reports/_page_nav.html.slim
index 494469d..bfea6bf 100644
--- a/app/views/reports/_page_nav.html.slim
+++ b/app/views/reports/_page_nav.html.slim
@@ -9,19 +9,20 @@
- if @page_nav_mode != :comment || current_page.nil?
- if report.pages.any?
nav.mt-3 id=dom_id(report, :page_nav_spy)
- ul
+ ul data={ controller: :sortable }
- report.pages.each do |page|
- is_current = current_page == page
- li
+ li data={ "sortable-url": page_path(page), "form-name": "page", "position-attribute": "position" }
details.tree open=current_page_displayed(page) class=""
summary class=(is_current ? "active" : nil)
.content
i.bi.me-1 class="bi-file-earmark-check#{is_current ? "" : "" }"
- if is_current
- =< "#{page.position} #{page.path}"
+ span id=dom_id(page, :title) =< "#{page.position} #{page.path}"
- else
- =< link_to("#{page.position} #{page.path}", report_path(report, page_id: page.id), data: { "turbo-frame": :_top })
- ul id=dom_id(page, :page_nav_elements)
+ =< link_to(report_path(report, page_id: page.id), data: { "turbo-frame": :_top }) do
+ span id=dom_id(page, :title) =< "#{page.position} #{page.path}"
+ ul id=dom_id(page, :page_nav_elements) data={ controller: "sortable", "linked-element-id": "element_list" }
- page.elements.each do |element|
= render partial: "elements/page_nav_row", locals: { element: element, current_page: current_page == element.page }
/li
diff --git a/app/views/success_criteria/_success_criterion.html.slim b/app/views/success_criteria/_success_criterion.html.slim
index 8fd047d..1c05578 100644
--- a/app/views/success_criteria/_success_criterion.html.slim
+++ b/app/views/success_criteria/_success_criterion.html.slim
@@ -1,5 +1,5 @@
/ = turbo_frame_tag(dom_id(success_criterion, :frame)) do
- expanded = false unless defined?(expanded)
-details.success_criterion id="#{dom_id(success_criterion)}" class="#{success_criterion.result}"
+details.success_criterion id="#{dom_id(success_criterion)}" class="#{success_criterion.result}" data={ "sortable-url": success_criterion_path(success_criterion), "form-name": :success_criterion, "position-attribute": "position" }
== render partial: "success_criteria/header", locals: {success_criterion: success_criterion }
== render partial: "success_criteria/body", locals: {success_criterion: success_criterion }
\ No newline at end of file
diff --git a/app/views/success_criteria/update.turbo_stream.slim b/app/views/success_criteria/update.turbo_stream.slim
index 32d74ff..72fe6ae 100644
--- a/app/views/success_criteria/update.turbo_stream.slim
+++ b/app/views/success_criteria/update.turbo_stream.slim
@@ -1,2 +1,5 @@
= turbo_stream.replace dom_id(@success_criterion, :header), partial: "success_criteria/header", locals: {success_criterion: @success_criterion }
-= turbo_stream.replace dom_id(@success_criterion, :body), partial: "success_criteria/body", locals: {success_criterion: @success_criterion }
\ No newline at end of file
+= turbo_stream.replace dom_id(@success_criterion, :body), partial: "success_criteria/body", locals: {success_criterion: @success_criterion }
+
+- @success_criterion.element.success_criteria.each do |sc|
+ = turbo_stream.update(dom_id(sc, :position), sc.number)
\ No newline at end of file
diff --git a/db/migrate/20241111011637_remove_position_indices.rb b/db/migrate/20241111011637_remove_position_indices.rb
new file mode 100644
index 0000000..8856fff
--- /dev/null
+++ b/db/migrate/20241111011637_remove_position_indices.rb
@@ -0,0 +1,7 @@
+class RemovePositionIndices < ActiveRecord::Migration[8.0]
+ def change
+ remove_index :success_criteria, column: [:element_id, :position]
+ remove_index :elements, column: [:page_id, :position]
+ remove_index :pages, column: [:report_id, :position]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c5b61eb..6567ef5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2024_11_08_180654) do
+ActiveRecord::Schema[8.0].define(version: 2024_11_11_011637) do
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
t.text "body"
@@ -115,7 +115,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_08_180654) do
t.datetime "updated_at", null: false
t.integer "page_id", null: false
t.integer "position", null: false
- t.index ["page_id", "position"], name: "index_elements_on_page_id_and_position", unique: true
t.index ["page_id"], name: "index_elements_on_page_id"
end
@@ -145,7 +144,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_08_180654) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "notes"
- t.index ["report_id", "position"], name: "index_pages_on_report_id_and_position", unique: true
t.index ["report_id"], name: "index_pages_on_report_id"
end
@@ -192,7 +190,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_08_180654) do
t.integer "priority"
t.integer "position", null: false
t.index ["check_id"], name: "index_success_criteria_on_check_id"
- t.index ["element_id", "position"], name: "index_success_criteria_on_element_id_and_position", unique: true
t.index ["element_id"], name: "index_success_criteria_on_element_id"
end