diff --git a/app/commands/admin/reorder_scopes.rb b/app/commands/admin/reorder_scopes.rb new file mode 100644 index 0000000000..ea2a3e43bf --- /dev/null +++ b/app/commands/admin/reorder_scopes.rb @@ -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 diff --git a/app/forms/decidim/user_interest_scope_form.rb b/app/forms/decidim/user_interest_scope_form.rb new file mode 100644 index 0000000000..319d2a3506 --- /dev/null +++ b/app/forms/decidim/user_interest_scope_form.rb @@ -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 diff --git a/app/forms/decidim/user_interests_form.rb b/app/forms/decidim/user_interests_form.rb new file mode 100644 index 0000000000..6220393fa7 --- /dev/null +++ b/app/forms/decidim/user_interests_form.rb @@ -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 diff --git a/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb new file mode 100644 index 0000000000..15a273bd7c --- /dev/null +++ b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb @@ -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 diff --git a/app/packs/entrypoints/application.js b/app/packs/entrypoints/application.js index fc8ab3b017..1d2bfce7d8 100644 --- a/app/packs/entrypoints/application.js +++ b/app/packs/entrypoints/application.js @@ -17,3 +17,5 @@ // Activate Active Storage // import * as ActiveStorage from "@rails/activestorage" // ActiveStorage.start() + +import "src/decidim/admin/reorder_scopes"; diff --git a/app/packs/entrypoints/decidim_custom_scopes.scss b/app/packs/entrypoints/decidim_custom_scopes.scss new file mode 100644 index 0000000000..eace17edd8 --- /dev/null +++ b/app/packs/entrypoints/decidim_custom_scopes.scss @@ -0,0 +1 @@ +@import "stylesheets/decidim/scopes/scopes-custom.scss"; \ No newline at end of file diff --git a/app/packs/src/decidim/admin/reorder_scopes.js b/app/packs/src/decidim/admin/reorder_scopes.js new file mode 100644 index 0000000000..b997c3c509 --- /dev/null +++ b/app/packs/src/decidim/admin/reorder_scopes.js @@ -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 }) + }); + }) +}); \ No newline at end of file diff --git a/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss b/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss new file mode 100644 index 0000000000..fd6e3f4f1f --- /dev/null +++ b/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss @@ -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 +} \ No newline at end of file diff --git a/app/views/decidim/admin/scopes/index.html.erb b/app/views/decidim/admin/scopes/index.html.erb new file mode 100644 index 0000000000..4b9dce24ce --- /dev/null +++ b/app/views/decidim/admin/scopes/index.html.erb @@ -0,0 +1,65 @@ +<% add_decidim_page_title(t("decidim.admin.titles.scopes")) %> +
+
+
+
+
+

+ <% 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 %> +

+
+
+ <% if @scopes.any? %> +
+ + + + + + + + +
<%= t("models.scope.fields.name", scope: "decidim.admin") %><%= t("models.scope.fields.scope_type", scope: "decidim.admin") %>
+ + +
    "> + <% @scopes.each do |scope| %> +
  • +
    +
    + <%= icon "move", class: "icon--small", role: "img", "aria-hidden": true %> + <%= link_to translated_attribute(scope.name), scope_scopes_path(scope), class:"custom-text" %> +
    +
    + <%= 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 %> +
    +
    +
  • + <% end %> +
+ +
+
+ <% else %> +

<%= t("decidim.admin.scopes.no_scopes") %>

+ <% end %> +
+
+
+
+
+ +<%= stylesheet_pack_tag "decidim_custom_scopes", media: "all" %> +<%= javascript_pack_tag 'application' %> \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 80c2f74eaa..1411333f2d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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" diff --git a/config/locales/en.yml b/config/locales/en.yml index 56be2f097d..beafaddd2b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: @@ -175,6 +193,7 @@ en: change: Change choose: Choose currently_selected: Currently selected + prompt: Select a scope shared: login_modal: close_modal: Close modal diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6e725c5912..c5c91823bc 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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: @@ -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 diff --git a/db/migrate/20240412112810_add_weight_to_scopes.rb b/db/migrate/20240412112810_add_weight_to_scopes.rb new file mode 100644 index 0000000000..bbcbc86565 --- /dev/null +++ b/db/migrate/20240412112810_add_weight_to_scopes.rb @@ -0,0 +1,5 @@ +class AddWeightToScopes < ActiveRecord::Migration[6.1] + def change + add_column :decidim_scopes, :weight, :integer, default: 0 + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 4432bd4822..cbad410846 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb b/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb new file mode 100644 index 0000000000..3db4a906b1 --- /dev/null +++ b/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Admin + module ScopesControllerExtends + extend ActiveSupport::Concern + included do + def index + enforce_permission_to :read, :scope + @scopes = children_scopes.sort_by(&:weight) + end + + def update + enforce_permission_to :update, :scope, scope: scope + @form = form(ScopeForm).from_params(params) + + return update_scopes if params[:id] == "refresh_scopes" + + UpdateScope.call(scope, @form) do + on(:ok) do + flash[:notice] = I18n.t("scopes.update.success", scope: "decidim.admin") + redirect_to current_scopes_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("scopes.update.error", scope: "decidim.admin") + render :edit + end + end + end + + private + + def update_scopes + ::Admin::ReorderScopes.call(current_organization, :scopes, params[:manifests]) do + on(:ok) do + flash[:notice] = I18n.t("scopes.update.success", scope: "decidim.admin") + end + end + end + end + end + end +end + +Decidim::Admin::ScopesController.include(Decidim::Admin::ScopesControllerExtends) diff --git a/lib/extends/controllers/decidim/scopes_controller_extends.rb b/lib/extends/controllers/decidim/scopes_controller_extends.rb new file mode 100644 index 0000000000..62197557b9 --- /dev/null +++ b/lib/extends/controllers/decidim/scopes_controller_extends.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module ScopesControllerExtends + extend ActiveSupport::Concern + included do + def picker + enforce_permission_to :pick, :scope + + context = picker_context(root, title, max_depth) + required = params&.[](:required) != "false" + + scopes, parent_scopes = resolve_picker_scopes(root, current) + + render( + :picker, + layout: nil, + locals: { + required: required, + title: title, + root: root, + current: (current || root), + scopes: scopes&.sort_by(&:weight), + parent_scopes: parent_scopes.sort_by(&:weight), + picker_target_id: (params[:target_element_id] || "content"), + global_value: params[:global_value], + max_depth: max_depth, + context: context + } + ) + end + end +end + +Decidim::ScopesController.include(ScopesControllerExtends) diff --git a/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb b/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb new file mode 100644 index 0000000000..9688decdf4 --- /dev/null +++ b/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module CheckBoxesTreeHelperExtends + def filter_scopes_values + return filter_scopes_values_from_parent(current_component.scope) if current_component.scope.present? + + main_scopes = current_participatory_space.scopes.top_level + .includes(:scope_type, :children) + .sort_by(&:weight) + filter_scopes_values_from(main_scopes) + end + + def filter_scopes_values_from_parent(scope) + scopes_values = [] + scope.children.sort_by(&:weight).each do |child| + unless child.children + scopes_values << Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)) + next + end + scopes_values << Decidim::CheckBoxesTreeHelper::TreeNode.new( + Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)), + scope_children_to_tree(child) + ) + end + + filter_tree_from(scopes_values) + end + + def filter_scopes_values_from(scopes) + scopes_values = scopes.compact.sort_by(&:weight).flat_map do |scope| + Decidim::CheckBoxesTreeHelper::TreeNode.new( + Decidim::CheckBoxesTreeHelper::TreePoint.new(scope.id.to_s, translated_attribute(scope.name, current_participatory_space.organization)), + scope_children_to_tree(scope) + ) + end + + scopes_values.prepend(Decidim::CheckBoxesTreeHelper::TreePoint.new("global", t("decidim.scopes.global"))) if current_participatory_space.scope.blank? + + filter_tree_from(scopes_values) + end + + def scope_children_to_tree(scope) + return if scope.scope_type && scope.scope_type == current_participatory_space.try(:scope_type_max_depth) + return unless scope.children.any? + + sorted_children = scope.children.includes(:scope_type, :children).sort_by(&:weight) + + sorted_children.flat_map do |child| + Decidim::CheckBoxesTreeHelper::TreeNode.new( + Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)), + scope_children_to_tree(child) + ) + end + end +end + +Decidim::CheckBoxesTreeHelper.module_eval do + prepend(CheckBoxesTreeHelperExtends) +end diff --git a/spec/helpers/decidim/check_boxes_tree_helper_spec.rb b/spec/helpers/decidim/check_boxes_tree_helper_spec.rb new file mode 100644 index 0000000000..df858de861 --- /dev/null +++ b/spec/helpers/decidim/check_boxes_tree_helper_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe CheckBoxesTreeHelper do + let(:helper) do + Class.new(ActionView::Base) do + include CheckBoxesTreeHelper + include TranslatableAttributes + end.new(ActionView::LookupContext.new(ActionController::Base.view_paths), {}, []) + end + + let!(:organization) { create(:organization) } + let!(:participatory_space) { create(:participatory_process, organization: organization) } + let!(:component) { create(:component, participatory_space: participatory_space) } + + before do + allow(helper).to receive(:current_participatory_space).and_return(participatory_space) + allow(helper).to receive(:current_component).and_return(component) + end + + describe "#filter_scopes_values" do + let(:root) { helper.filter_scopes_values } + let(:leaf) { helper.filter_scopes_values.leaf } + let(:nodes) { helper.filter_scopes_values.node } + + context "when the participatory space does not have a scope" do + it "returns the global scope" do + expect(leaf.value).to eq("") + expect(nodes.count).to eq(1) + expect(nodes.first).to be_a(Decidim::CheckBoxesTreeHelper::TreePoint) + expect(nodes.first.value).to eq("global") + end + end + + context "when the participatory space has a scope with subscopes" do + let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) } + let!(:subscopes) { create_list :subscope, 5, parent: participatory_space.scope } + + it "returns all the subscopes" do + expect(leaf.value).to eq("") + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(root.node.count).to eq(5) + end + end + + context "when the component does not have a scope" do + before do + component.update!(settings: { scopes_enabled: true, scope_id: nil }) + end + + it "returns the global scope" do + expect(leaf.value).to eq("") + expect(nodes.count).to eq(1) + expect(nodes.first).to be_a(Decidim::CheckBoxesTreeHelper::TreePoint) + expect(nodes.first.value).to eq("global") + end + end + + context "when the component has a scope with subscopes" do + let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) } + let!(:subscopes) { create_list :subscope, 5, parent: participatory_space.scope } + + before do + component.update!(settings: { scopes_enabled: true, scope_id: participatory_space.scope.id }) + end + + it "returns all the subscopes" do + expect(leaf.value).to eq("") + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(root.node.count).to eq(5) + end + end + end + + context "when there is weight in the scopes" do + let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) } + let!(:subscopes) { create_list(:subscope, 5, parent: participatory_space.scope) } + + before do + subscopes.shuffle.each_with_index { |subscope, index| subscope.update!(weight: index) } + end + + it "returns the subscopes sorted by weight" do + expected_ids = subscopes.sort_by(&:weight).map { |subscope| subscope.id.to_s } + actual_values = helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s } + expect(actual_values).to eq(expected_ids) + end + + it "assigns weights correctly after shuffle" do + weights = subscopes.map(&:weight) + expect(weights).to match_array([0, 1, 2, 3, 4]) + end + + it "sorts subscopes correctly by weight" do + sorted_subscopes = subscopes.sort_by(&:weight) + expect(subscopes.sort_by(&:weight)).to eq(sorted_subscopes) + end + + it "checks that the helper method returns sorted subscopes" do + sorted_subscopes = subscopes.sort_by(&:weight).map { |subscope| subscope.id.to_s } + expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).to eq(sorted_subscopes) + end + + it "returns false when the subscopes are not sorted by weight" do + unsorted_subscopes = subscopes.shuffle + unsorted_values = unsorted_subscopes.map { |subscope| subscope.id.to_s } + expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).not_to eq(unsorted_values) + end + + it "returns false when subscopes are not sorted in ascending order of weight" do + reversed_subscopes = subscopes.sort_by(&:weight).reverse + reversed_values = reversed_subscopes.map { |subscope| subscope.id.to_s } + expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).not_to eq(reversed_values) + end + end + end +end diff --git a/spec/system/admin_manages_organization_scopes_spec.rb b/spec/system/admin_manages_organization_scopes_spec.rb new file mode 100644 index 0000000000..c1e28bbc31 --- /dev/null +++ b/spec/system/admin_manages_organization_scopes_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Organization scopes", type: :system do + include Decidim::SanitizeHelper + + let(:organization) { create :organization, default_locale: :en, available_locales: [:en, :es, :ca, :fr] } + let(:admin) { create :user, :admin, :confirmed, organization: organization } + let!(:attributes) { attributes_for(:scope) } + + before do + switch_to_host(organization.host) + end + + describe "Managing scopes" do + let!(:scope_type) { create(:scope_type, organization: admin.organization) } + + before do + login_as admin, scope: :user + visit decidim_admin.root_path + click_link "Settings" + click_link "Scopes" + end + + it "can create new scopes" do + click_link "Add" + + within ".new_scope" do + fill_in_i18n :scope_name, "#scope-name-tabs", **attributes[:name].except("machine_translations") + fill_in "Code", with: "MY-DISTRICT" + select scope_type.name["en"], from: :scope_scope_type_id + + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within ".draggable-list" do + expect(page).to have_content(translated(attributes[:name])) + end + + visit decidim_admin.root_path + expect(page).to have_content("created the #{translated(attributes[:name])} scope") + end + + context "with existing scopes" do + let!(:scope) { create(:scope, organization: organization) } + + before do + visit current_path + end + + it "can edit them" do + within find(".draggable-content", text: translated(scope.name)) do + click_link "Edit" + end + + within ".edit_scope" do + fill_in_i18n :scope_name, "#scope-name-tabs", **attributes[:name].except("machine_translations") + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within ".draggable-list" do + expect(page).to have_content(translated(attributes[:name])) + end + + visit decidim_admin.root_path + expect(page).to have_content("updated the #{translated(attributes[:name])} scope") + end + + it "can delete them" do + within find(".draggable-content", text: translated(scope.name)) do + accept_confirm { click_link "Destroy" } + end + + expect(page).to have_admin_callout("successfully") + + within ".card-section" do + expect(page).to have_no_content(translated(scope.name)) + end + end + + it "can create a new subcope" do + within find(".draggable-content", text: translated(scope.name)) do + find("a", text: translated(scope.name)).click + end + + click_link "Add" + + within ".new_scope" do + fill_in_i18n :scope_name, "#scope-name-tabs", en: "My nice subdistrict", + es: "Mi lindo subdistrito", + ca: "El meu bonic subbarri" + fill_in "Code", with: "MY-SUBDISTRICT" + select scope_type.name["en"], from: :scope_scope_type_id + + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within ".draggable-list" do + expect(page).to have_content("My nice subdistrict") + end + end + end + end +end