Skip to content

Commit

Permalink
backport: Addition of sortable scopes using drag and drop (#632)
Browse files Browse the repository at this point in the history
* backport: Backport the Drag & Drop on Scopes on BackOffice

* fix: Fix locales that were not normalized or misplaced

* test: Add some specs about the check_boxes_tree_helper sort

* test: Add specs releated to the backport scopes management

* fix: Fix wrong locales

* fix: Add the weight sorting on select input of scopes

* fix: Add missing locale
  • Loading branch information
AyakorK authored Nov 22, 2024
1 parent c66b9c0 commit 337c338
Show file tree
Hide file tree
Showing 19 changed files with 658 additions and 0 deletions.
37 changes: 37 additions & 0 deletions app/commands/admin/reorder_scopes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Admin
class ReorderScopes < Decidim::Command
def initialize(organization, scope, ids)
@organization = organization
@scope = scope
@ids = ids
end

def call
return broadcast(:invalid) if @ids.blank?

reorder_scopes
broadcast(:ok)
end

def collection
@collection ||= Decidim::Scope.where(id: @ids, organization: @organization)
end

def reorder_scopes
transaction do
set_new_weights
end
end

def set_new_weights
@ids.each do |id|
current_scope = collection.find { |block| block.id == id.to_i }
next if current_scope.blank?

current_scope.update!(weight: @ids.index(id) + 1)
end
end
end
end
25 changes: 25 additions & 0 deletions app/forms/decidim/user_interest_scope_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Decidim
# The form object that handles the data behind updating a user's
# interests in their profile page.
class UserInterestScopeForm < Form
mimic :scope

attribute :name, JsonbAttributes
attribute :checked, Boolean
attribute :children, Array[UserInterestScopeForm]

def map_model(model_hash)
scope = model_hash[:scope]
user = model_hash[:user]

self.id = scope.id
self.name = scope.name
self.checked = user.interested_scopes_ids.include?(scope.id)
self.children = scope.children.sort_by(&:weight).map do |children_scope|
UserInterestScopeForm.from_model(scope: children_scope, user: user)
end
end
end
end
23 changes: 23 additions & 0 deletions app/forms/decidim/user_interests_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Decidim
# The form object that handles the data behind updating a user's
# interests in their profile page.
class UserInterestsForm < Form
mimic :user

attribute :scopes, Array[UserInterestScopeForm]

def newsletter_notifications_at
return unless newsletter_notifications

Time.current
end

def map_model(user)
self.scopes = user.organization.scopes.top_level.sort_by(&:weight).map do |scope|
UserInterestScopeForm.from_model(scope: scope, user: user)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module Decidim
module SimpleProposal
module ScopesHelperOverride
extend ActiveSupport::Concern

included do
def scopes_picker_field(form, name, root: false, options: { checkboxes_on_top: true, sort_by_weight: true })
options.merge!(selected: selected_scope(form)) if selected_scope(form)
form.select(name, simple_scope_options(root: root, options: options), include_blank: t("decidim.scopes.prompt"))
end

private

def selected_scope(form)
form.try(:scope_id) ||
form.try(:settings).try(:scope_id) ||
form.try(:object).try(:scope_id) ||
form.try(:object).try(:decidim_scope_id)
end

def simple_scope_options(root: false, options: {})
scopes_array = []
roots = root ? root.children : ancestors

roots.sort_by { |ancestor| ancestor.weight || 0 }.each do |ancestor|
children_after_parent(ancestor, scopes_array, "")
end

selected = options.has_key?(:selected) ? options[:selected] : params.dig(:filter, :decidim_scope_id)
options_for_select(scopes_array, selected)
end

def ancestors
@ancestors ||= current_organization.scopes.where(parent_id: nil)
end

def children_after_parent(ancestor, array, prefix)
array << ["#{prefix} #{translated_attribute(ancestor.name)}", ancestor.id, ancestor.weight]
ancestor.children.sort_by { |child| child.weight || 0 }.each do |child|
children_after_parent(child, array, "#{prefix}-")
end
end
end
end
end
end
2 changes: 2 additions & 0 deletions app/packs/entrypoints/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@
// Activate Active Storage
// import * as ActiveStorage from "@rails/activestorage"
// ActiveStorage.start()

import "src/decidim/admin/reorder_scopes";
1 change: 1 addition & 0 deletions app/packs/entrypoints/decidim_custom_scopes.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "stylesheets/decidim/scopes/scopes-custom.scss";
19 changes: 19 additions & 0 deletions app/packs/src/decidim/admin/reorder_scopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
$(document).ready(() => {
let activeBlocks = Array.prototype.slice.call(document.querySelectorAll(".js-list-scopes li"));
const defaultOrder = activeBlocks.map(block => block.dataset.scopeId);

document.addEventListener("dragend", () => {
activeBlocks = Array.prototype.slice.call(document.querySelectorAll(".js-list-scopes li"));
let activeBlocksManifestName = activeBlocks.map(block => block.dataset.scopeId);
let sortUrl = document.querySelector(".js-list-scopes").dataset.sortUrl;

if (JSON.stringify(activeBlocksManifestName) === JSON.stringify(defaultOrder)) { return; }

$.ajax({
method: "PUT",
url: sortUrl,
contentType: "application/json",
data: JSON.stringify({ manifests: activeBlocksManifestName })
});
})
});
18 changes: 18 additions & 0 deletions app/packs/stylesheets/decidim/scopes/_scopes-custom.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.draggable-list .draggable-content {
cursor: move;
justify-content: space-between;
align-items: center;
font-weight: 600;
border: none !important;
background-color: transparent !important;
padding: 0.5rem 1rem;
}

.custom-text {
color: black;
}

.custom-list {
border: 1px solid lightgray !important;
margin: 0.4rem
}
65 changes: 65 additions & 0 deletions app/views/decidim/admin/scopes/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<% add_decidim_page_title(t("decidim.admin.titles.scopes")) %>
<div class="grid-container full">
<div class="grid-x grid-margin-x card-grid">
<div class="cell">
<div class="card">
<div class="card-divider">
<h2 class="card-title">
<% if parent_scope %>
<%= scope_breadcrumbs(parent_scope).join(" - ").html_safe %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_scope_path(parent_scope), class: "button tiny button--title" if allowed_to? :create, :scope %><%= link_to t("actions.edit", scope: "decidim.admin"), edit_scope_path(parent_scope), class: "button tiny button--title" if allowed_to? :edit, :scope, scope: parent_scope %>
<% else %>
<%= t "decidim.admin.titles.scopes" %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_path, class: "button tiny button--title" if allowed_to? :create, :scope %>
<% end %>
</h2>
</div>
<div class="card-section">
<% if @scopes.any? %>
<div class="table-scroll">
<table class="table-list">
<thead>
<tr>
<th><%= t("models.scope.fields.name", scope: "decidim.admin") %></th>
<th><%= t("models.scope.fields.scope_type", scope: "decidim.admin") %></th>
<th></th>
</tr>
</thead>
</table>
<table>
<tbody>
<ul class="draggable-list js-connect js-list-scopes" data-sort-url="<%= "/admin/scopes/refresh_scopes" %>">
<% @scopes.each do |scope| %>
<li draggable="true" data-scope-id="<%= scope.id %>" class="custom-list">
<div class="draggable-content">
<div>
<%= icon "move", class: "icon--small", role: "img", "aria-hidden": true %>
<%= link_to translated_attribute(scope.name), scope_scopes_path(scope), class:"custom-text" %>
</div>
<div>
<%= icon_link_to "zoom-in", scope_scopes_path(scope), t("actions.browse", scope: "decidim.admin"), class: "action-icon--browse", method: :get, data: {} %>

<% if allowed_to? :update, :scope, scope: scope %>
<%= icon_link_to "pencil", [:edit, scope], t("actions.edit", scope: "decidim.admin"), class: "action-icon--edit", method: :get, data: {} %>
<% end %>

<% if allowed_to? :destroy, :scope, scope: scope %>
<%= icon_link_to "circle-x", scope, t("actions.destroy", scope: "decidim.admin"), class: "action-icon--remove", method: :delete, data: { confirm: t("actions.confirm_destroy", scope: "decidim.admin") } %>
<% end %>
</div>
</div>
</li>
<% end %>
</ul>
</tbody>
</table>
</div>
<% else %>
<p><%= t("decidim.admin.scopes.no_scopes") %></p>
<% end %>
</div>
</div>
</div>
</div>
</div>

<%= stylesheet_pack_tag "decidim_custom_scopes", media: "all" %>
<%= javascript_pack_tag 'application' %>
3 changes: 3 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ class Application < Rails::Application
config.after_initialize do
require "extends/controllers/decidim/devise/sessions_controller_extends"
require "extends/controllers/decidim/editor_images_controller_extends"
require "extends/controllers/decidim/admin/scopes_controller_extends"
require "extends/controllers/decidim/scopes_controller_extends"
require "extends/services/decidim/iframe_disabler_extends"
require "extends/helpers/decidim/check_boxes_tree_helper_extends"
require "extends/helpers/decidim/icon_helper_extends"
require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends"
require "extends/controllers/decidim/initiatives/committee_requests_controller_extends"
Expand Down
19 changes: 19 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,31 @@ en:
file: importing file
decidim:
admin:
actions:
add: Add
browse: Browse
confirm_destroy: Confirm destroy
destroy: Destroy
edit: Edit
exports:
export_as: "%{name} as %{export_format}"
notice: Your export is currently in progress. You'll receive an email when it's complete.
models:
scope:
fields:
name: Name
scope_type: Scope type
participatory_space_private_users:
create:
error: Error
success: Success
scopes:
no_scopes: No scopes at this level.
update:
error: There was a problem updating this scope.
success: Scope updated successfully
titles:
scopes: Scopes
amendments:
emendation:
announcement:
Expand Down Expand Up @@ -175,6 +193,7 @@ en:
change: Change
choose: Choose
currently_selected: Currently selected
prompt: Select a scope
shared:
login_modal:
close_modal: Close modal
Expand Down
19 changes: 19 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,33 @@ fr:
file: importer un fichier d'utilisateurs
decidim:
admin:
actions:
add: Ajouter
browse: Naviguer
confirm_destroy: Confirmer la suppression
destroy: Supprimer
edit: Modifier
exports:
export_as: "%{name} au format %{export_format}"
notice: Votre exportation est en cours. Vous recevrez un e-mail quand elle sera terminée.
menu:
admin_accountability: Admin accountability
models:
scope:
fields:
name: Nom
scope_type: Type de secteur
participatory_space_private_users:
create:
error: Erreur
success: Succès
scopes:
no_scopes: Aucun secteur à ce niveau.
update:
error: Il y a eu une erreur lors de la mise à jour du secteur.
success: Secteur mis à jour avec succès.
titles:
scopes: Secteurs
amendments:
emendation:
announcement:
Expand Down Expand Up @@ -177,6 +195,7 @@ fr:
change: Modifier
choose: Sélectionner
currently_selected: Sélectionné
prompt: Sélectionnez un périmètre d'application
shared:
login_modal:
close_modal: Fermer
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20240412112810_add_weight_to_scopes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddWeightToScopes < ActiveRecord::Migration[6.1]
def change
add_column :decidim_scopes, :weight, :integer, default: 0
end
end
1 change: 1 addition & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1766,6 +1766,7 @@
t.string "code", null: false
t.integer "part_of", default: [], null: false, array: true
t.jsonb "geojson"
t.integer "weight", default: 0
t.index ["decidim_organization_id", "code"], name: "index_decidim_scopes_on_decidim_organization_id_and_code", unique: true
t.index ["decidim_organization_id"], name: "index_decidim_scopes_on_decidim_organization_id"
t.index ["parent_id"], name: "index_decidim_scopes_on_parent_id"
Expand Down
Loading

0 comments on commit 337c338

Please sign in to comment.