diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index cf5d921..0dcc16b 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -372,6 +372,9 @@ table.items td.actions {
gap: 0.4em;
justify-content: end;
}
+table.items td.handle {
+ cursor: move;
+}
/* TODO: replace :hover:focus-visible combos with proper LOVE stye order */
/* TODO: Update styling, including rem removal. */
diff --git a/app/controllers/units_controller.rb b/app/controllers/units_controller.rb
index bae6949..b6c60a0 100644
--- a/app/controllers/units_controller.rb
+++ b/app/controllers/units_controller.rb
@@ -9,7 +9,7 @@ class UnitsController < ApplicationController
end
def index
- @units = current_user.units
+ @units = current_user.units.includes(:subunits)
end
def new
diff --git a/app/models/unit.rb b/app/models/unit.rb
index 8764a43..3ab7682 100644
--- a/app/models/unit.rb
+++ b/app/models/unit.rb
@@ -2,6 +2,7 @@ class Unit < ApplicationRecord
belongs_to :user, optional: true
# TODO: validate base.user == user
belongs_to :base, optional: true, class_name: "Unit"
+ has_many :subunits, class_name: "Unit", dependent: :restrict_with_error, inverse_of: :base
validates :symbol, presence: true, uniqueness: {scope: :user_id},
length: {maximum: columns_hash['symbol'].limit}
@@ -18,11 +19,16 @@ class Unit < ApplicationRecord
'COALESCE',
[Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]]
)
- left_outer_joins(:base).order(parent_symbol, arel_table[:base_id].asc.nulls_first, :multiplier)
+ left_outer_joins(:base)
+ .order(parent_symbol, arel_table[:base_id].asc.nulls_first, :multiplier, :symbol)
}
before_destroy do
# TODO: disallow destruction if any object depends on this unit
nil
end
+
+ def movable?
+ subunits.empty?
+ end
end
diff --git a/app/views/units/_unit.html.erb b/app/views/units/_unit.html.erb
index 754a43b..5ac00eb 100644
--- a/app/views/units/_unit.html.erb
+++ b/app/views/units/_unit.html.erb
@@ -1,4 +1,6 @@
-
+<%= tag.tr id: dom_id(unit), ondragover: "dragOver(event)", ondrop: "drop(event)",
+ data: {drag_path: unit_path(unit), drop_id: unit.base_id || unit.id} do %>
+
<%= link_to unit.symbol, edit_unit_path(unit), id: dom_id(unit, :edit),
onclick: 'this.blur();', data: {turbo_stream: true} %>
@@ -17,5 +19,10 @@
<%= image_button_to t(".delete_unit"), "delete-outline", unit_path(unit),
method: :delete %>
|
+ <% if unit.movable? %>
+ ⠿ |
+ <% else %>
+ |
+ <% end %>
<% end %>
-
+<% end %>
diff --git a/app/views/units/index.html.erb b/app/views/units/index.html.erb
index 5ebd3e6..42ea279 100644
--- a/app/views/units/index.html.erb
+++ b/app/views/units/index.html.erb
@@ -15,6 +15,7 @@
<%= User.human_attribute_name(:multiplier).capitalize %> |
<% if current_user.at_least(:active) %>
<%= t :actions %> |
+ |
<% end %>
@@ -29,4 +30,36 @@
event.target.closest("tr").querySelector("a[name=cancel]").click();
}
}
+
+ function dragStart(event) {
+ var row = event.target.closest("tr");
+ event.dataTransfer.setData("text/plain", row.getAttribute("data-drag-path"));
+ var rowRectangle = row.getBoundingClientRect();
+ event.dataTransfer.setDragImage(row, event.x - rowRectangle.left, event.y - rowRectangle.top);
+ event.dataTransfer.dropEffect = "none";
+ }
+
+ function dragOver(event) {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ }
+
+ function drop(event) {
+ event.preventDefault();
+
+ var params = new URLSearchParams();
+ params.append("unit[base_id]", event.target.closest("tr").getAttribute("data-drop-id"));
+
+ fetch(event.dataTransfer.getData("text/plain"), {
+ body: params,
+ headers: {
+ "Accept": "text/vnd.turbo-stream.html",
+ "X-CSRF-Token": document.head.querySelector("meta[name=csrf-token]").content,
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ method: "PATCH"
+ })
+ .then(response => response.text())
+ .then(html => Turbo.renderStreamMessage(html))
+ }
<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index a2dd6a6..8710f5b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -22,6 +22,9 @@ en:
actioncontroller:
exceptions:
status:
+ bad_request: >
+ Server received request it's unable to understand (400 Bad Request).
+ This should not happen, please notify site administrator.
forbidden: >
You have not been granted access to this action (403 Forbidden).
This should not happen, please notify site administrator.