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