Skip to content

Commit

Permalink
Improve experience, usability giving proposals minimum votes number (#…
Browse files Browse the repository at this point in the history
…13349)

Co-authored-by: Alexandru Emil Lupu <contact@alecslupu.ro>
  • Loading branch information
2 people authored and mllocs committed Jan 9, 2025
1 parent 3f2cf02 commit b9a4ec9
Show file tree
Hide file tree
Showing 21 changed files with 534 additions and 33 deletions.
4 changes: 4 additions & 0 deletions decidim-core/app/helpers/decidim/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ module ApplicationHelper
include Decidim::AmendmentsHelper
include Decidim::CacheHelper

def layout_item_classes
"layout-item"
end

# Truncates a given text respecting its HTML tags.
#
# text - The String text to be truncated.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div role="complementary">
<%= yield :item_header %>
</div>
<div class="layout-item">
<div class="<%= layout_item_classes %>">
<main class="layout-item__main">
<% if params[:included_in] %>
<%= render partial: "layouts/decidim/shared/linked_resource" %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ def votes_given
).count
end

def layout_item_classes
"layout-item lg:pt-4"
end

def show_voting_rules?
return false if !votes_enabled? || current_settings.votes_blocked?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ def remaining_votes_count_for(user)
votes_count = ProposalVote.where(author: user, proposal: proposals).size
component_settings.vote_limit - votes_count
end

# Return the remaining minimum votes for a user if the current component has a vote limit
#
# user - A User object
#
# Returns a number with the remaining minimum votes for that user
def remaining_minimum_votes_count_for(user)
return 0 unless vote_limit_enabled?

votes_count = Decidim::Proposals::ProposalVote.joins(:proposal).where(decidim_proposals_proposals: { decidim_component_id: current_component.id }).where(author: user).count

component_settings.minimum_votes_per_user - votes_count
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "src/decidim/proposals/utils"
import "src/decidim/proposals/add_proposal"
import "src/decidim/proposals/choose_proposals"
import "src/decidim/proposals/exit_handler"

// Images
require.context("../images", true)
Expand Down
73 changes: 73 additions & 0 deletions decidim-proposals/app/packs/src/decidim/proposals/exit_handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const allowExitFrom = (el) => {
if (el.id === "exit-proposal-notification-link" || el.classList.contains("no-modal")) {
return true;
}

return false;
};

document.addEventListener("DOMContentLoaded", () => {
const exitNotification = document.getElementById("exit-proposal-notification");
const exitLink = document.getElementById("exit-proposal-notification-link");
if (!exitLink) {
return;
}
const defaultExitUrl = exitLink.href;
const defaultExitLinkText = exitLink.textContent;
const signOutPath = window.Decidim.config.get("sign_out_path");
let exitLinkText = defaultExitLinkText;

if (!exitNotification) {
// Do not apply when not inside the voting pipeline
return;
}

const openExitNotification = (url, method = null) => {
if (method && method !== "get") {
exitLink.setAttribute("data-method", method);
} else {
exitLink.removeAttribute("data-method");
}

exitLink.setAttribute("href", url);
exitLink.textContent = exitLinkText;
window.Decidim.currentDialogs["exit-proposal-notification"].open();
};

const handleClicks = (link) => {
link.addEventListener("click", (event) => {
exitLinkText = defaultExitLinkText;

if (
!allowExitFrom(link) &&
((window.Decidim.currentDialogs["exit-proposal-notification"].dialog.querySelector("[data-dialog-container]")).dataset.minimumVotesReached !== "true") &&
((window.Decidim.currentDialogs["exit-proposal-notification"].dialog.querySelector("[data-dialog-container]")).dataset.minimumVotesCount > 0)
) {
event.preventDefault();
openExitNotification(link.getAttribute("href"), link.dataset.method);
}
});
};

document.querySelectorAll("a").forEach(handleClicks);
// Custom handling for the header sign-out link
const signOutLink = document.querySelector(`[href='${signOutPath}']`);
if (signOutLink) {
signOutLink.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();

exitLinkText = signOutLink.textContent;
openExitNotification(signOutLink.getAttribute("href"), signOutLink.dataset.method);
});
}

// Custom handling for links that open the exit notification dialog
const dialogOpenLinks = document.querySelectorAll("a[data-dialog-open='exit-proposal-notification']");
dialogOpenLinks.forEach((link) => {
link.addEventListener("click", () => {
exitLinkText = defaultExitLinkText;
openExitNotification(defaultExitUrl);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,46 @@
}
}
}

#proposal-voting-rules {
[id="dropdown-menu-proposal-voting-rules"] {
&[aria-hidden="true"] {
@apply hidden;
}
}

[data-target="dropdown-menu-proposal-voting-rules"] {
@apply w-full flex items-center justify-between gap-2 p-2 first-of-type:[&>svg]:block last-of-type:[&>svg]:hidden;

& > span {
@apply hidden font-semibold text-secondary;

&:only-of-type,
&.is-active {
@apply flex items-center gap-2 [&_svg]:fill-current;
}
}

> svg {
@apply w-6 h-6 flex-none text-secondary fill-current;
}

&[aria-expanded="false"] > svg:last-of-type,
&[aria-expanded="true"] > svg:first-of-type {
@apply hidden;
}

&[aria-expanded="true"] > svg:last-of-type,
&[aria-expanded="false"] > svg:first-of-type {
@apply block;
}
}
}

.layout-item-complementary {
@apply container grid grid-cols-1 lg:grid-cols-12;

.item_voting_rules {
@apply lg:col-start-2 lg:col-span-10;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
</strong>
</div>

<div class="flash flex-col " style="<%= "background-color: #{form.object.bg_color}; color: #{form.object.text_color}; border-color: #{form.object.text_color} / var(--tw-border-opacity);" %>" data-announcement-preview>
<div class="flash flex-col" style="<%= "background-color: #{form.object.bg_color}; color: #{form.object.text_color}; border-color: #{form.object.text_color} / var(--tw-border-opacity);" %>" data-announcement-preview>
<div class="flash__title">
<%= decidim_escape_translated(form.object.announcement_title).presence || t(".preview") %>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
var $remainingVotesCount = $('#remaining-votes-count');
var $notVotedButtons = $('.card__button.button').not('.success');

if(!$remainingVotesCount[0]) { return; }

morphdom($remainingVotesCount[0], '<%= j(render partial: "decidim/proposals/proposals/remaining_votes_count").strip.html_safe %>');
if($remainingVotesCount[0]) {
morphdom($remainingVotesCount[0], '<%= j(render partial: "decidim/proposals/proposals/remaining_votes_count").strip.html_safe %>');
}

<% if remaining_votes_count_for(current_user) == 0 %>
$notVotedButtons.attr('disabled', true);
Expand All @@ -35,8 +35,28 @@

<% if show_voting_rules? %>
(function() {
var $remainingVotesNotification = $('#remaining-votes-notification');
var $exitProposalModal = $('#exit-proposal-notification');
var $votingRules = $('.voting-rules');
if(!$votingRules[0]) { return; }
morphdom($votingRules[0], '<%= j(render partial: "decidim/proposals/proposals/voting_rules").strip.html_safe %>');
var $votingRulesNode = $('.item_voting_rules')

if($votingRules[0]) {
morphdom($votingRules[0], '<%= j(render partial: "decidim/proposals/proposals/voting_rules").strip.html_safe %>');
}

<% if remaining_minimum_votes_count_for(current_user).zero? %>
$votingRulesNode.prepend(`<%= j(render partial: "decidim/proposals/proposals/notification_alert_box").strip.html_safe %>`)

<% end %>
if ($exitProposalModal[0]) {
morphdom($exitProposalModal[0], '<%= j(render partial: "decidim/proposals/proposals/exit_modal").strip.html_safe %>')
if(window.Decidim.currentDialogs["exit-proposal-notification"] && window.Decidim.currentDialogs["exit-proposal-notification"].dialog) {
window.Decidim.currentDialogs["exit-proposal-notification"].dialog.innerHTML = $exitProposalModal[0].innerHTML;
}
}

if ($remainingVotesNotification[0]) {
morphdom($remainingVotesNotification[0], '<%= j(render partial: "decidim/proposals/proposals/remaining_votes_notification").strip.html_safe %>')
}
}());
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<%= decidim_modal id: "exit-proposal-notification" do %>
<div data-dialog-container data-minimum-votes-reached="<%= remaining_minimum_votes_count_for(current_user).zero? %>" data-minimum-votes-count="<%= minimum_votes_per_user - remaining_minimum_votes_count_for(current_user) %>">
<%= icon "information-line" %>
<h3 class="h3 text-base" id="dialog-title-exit-proposal-notification" tabindex="-1" data-dialog-title>
<%= t("title", scope: "decidim.proposals.exit_modal", number: remaining_minimum_votes_count_for(current_user)) %>
</h3>
<p id="dialog-desc-budget-modal">
<%= t("message", scope: "decidim.proposals.exit_modal", number: remaining_minimum_votes_count_for(current_user)) %>
</p>
</div>
<div data-dialog-actions>
<button data-dialog-close="exit-proposal-notification" class="button button__sm md:button__lg button__secondary">
<%= t("cancel", scope: "decidim.proposals.exit_modal") %>
</button>
<%= link_to t("exit", scope: "decidim.proposals.exit_modal"), proposals_path, id: "exit-proposal-notification-link", class: "button button__sm md:button__lg button__secondary" %>
</div>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= alert_box(t("success", scope: "decidim.proposals.proposals.voting_rules"), :success, true, { id: "success-proposal-vote" }) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div id="proposal-voting-rules" class="flash flex-col p-0 gap-0">
<button id="dropdown-trigger-proposal-voting-rules" data-component="dropdown" data-target="dropdown-menu-proposal-voting-rules" class="flash__title">
<%= t("title", scope: "decidim.proposals.proposals.voting_rules") %>
<%= icon "arrow-down-s-line" %>
<%= icon "arrow-up-s-line" %>
</button>

<div class="flash__message">
<div class="text-base editor-content">
<ul id="dropdown-menu-proposal-voting-rules" class="mt-0 mb-2">
<% if vote_limit_enabled? %>
<li><%= t("vote_limit.description", scope: "decidim.proposals.proposals.voting_rules", limit: component_settings.vote_limit) %></li>
<% end %>

<% if proposal_limit_enabled? %>
<li><%= t("proposal_limit.description", scope: "decidim.proposals.proposals.voting_rules", limit: proposal_limit) %></li>
<% end %>

<% if threshold_per_proposal_enabled? %>
<li><%= t("threshold_per_proposal.description", scope: "decidim.proposals.proposals.voting_rules", limit: threshold_per_proposal) %></li>
<% end %>

<% if can_accumulate_votes_beyond_threshold? %>
<li><%= t("can_accumulate_votes_beyond_threshold.description", scope: "decidim.proposals.proposals.voting_rules", limit: threshold_per_proposal) %></li>
<% end %>

<% if minimum_votes_per_user_enabled? %>
<li><%= t("minimum_votes_per_user.description", scope: "decidim.proposals.proposals.voting_rules", votes: minimum_votes_per_user) %></li>
<% end %>
</ul>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div id="remaining-votes-count">
<%= t("votes", scope: "decidim.proposals.proposals.voting_rules.vote_limit", number: remaining_votes_count_for(current_user)) %>
<%= t("votes", scope: "decidim.proposals.proposals.voting_rules.vote_limit", number: remaining_minimum_votes_count_for(current_user)) %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= alert_box(
message(
content_tag(:div, class: "") do
concat content_tag(:strong, t("already_vote.title", scope: "decidim.proposals.proposals.voting_rules", number: remaining_minimum_votes_count_for(current_user)))
concat content_tag(:p, t("already_vote.description", scope: "decidim.proposals.proposals.voting_rules", number: remaining_minimum_votes_count_for(current_user)))
concat link_to(t("already_vote.see_other_proposals", scope: "decidim.proposals.proposals.voting_rules"), proposals_path, class: "no-modal")
end
),
:secondary,
true,
{ id: "remaining-votes-notification", class: "flash secondary #{"hidden" unless remaining_minimum_votes_count_for(current_user).positive?}" }
) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<% if show_voting_rules? %>
<div class="item_voting_rules">
<%= render partial: "remaining_votes_notification" %>
<%= render partial: "proposal_voting_rules" %>
</div>
<% end %>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<% if show_voting_rules? %>
<% announcement_body = capture do %>
<div class="editor-content">
<ul>
<ul class="mt-0 mb-2">
<% if vote_limit_enabled? %>
<li><%= t(".vote_limit.description", limit: component_settings.vote_limit) %></li>
<% end %>
Expand All @@ -21,15 +21,9 @@
<% if minimum_votes_per_user_enabled? %>
<li>
<%= t(".minimum_votes_per_user.description", votes: minimum_votes_per_user) %>
<% if votes_given >= minimum_votes_per_user %>
<%= t(".minimum_votes_per_user.given_enough_votes") %>
<% else %>
<%= t(".minimum_votes_per_user.votes_remaining", remaining_votes: minimum_votes_per_user - votes_given) %>
<% end %>
</li>
<% end %>
</ul>
<%= render partial: "decidim/proposals/proposals/remaining_votes_count" if current_user_can_vote? %>
</div>
<% end %>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,22 @@ extra_admin_link(
<% append_stylesheet_pack_tag "decidim_proposals", media: "all" %>
<% append_javascript_pack_tag "decidim_proposals" %>

<% content_for :item_header do %>
<div class="<%= "layout-item-complementary" if show_voting_rules? %>">
<%= render partial: "update_proposal_voting_rules" %>
</div>
<% end %>

<% content_for :aside do %>
<%= render partial: "proposal_aside" %>
<% end %>

<%= render layout: "layouts/decidim/shared/layout_item", locals: { back_path: component_settings.participatory_texts_enabled? ? main_component_path(current_component) : proposals_path } do %>

<section class="layout-main__section layout-main__heading">
<%= render partial: "voting_rules" %>
<% if show_voting_rules? && remaining_minimum_votes_count_for(current_user).positive? && current_component.participatory_space.can_participate?(current_user) %>
<%= render partial: "exit_modal" %>
<% end %>

<%= cell("decidim/announcement", proposal_reason_callout_announcement, callout_styles: @proposal.proposal_state&.css_style) if @proposal.answered? && @proposal.published_state? %>

Expand Down Expand Up @@ -107,10 +115,10 @@ extra_admin_link(
<%= content_tag :li, resource_reference(@proposal), class: "metadata__item" %>
<%= content_tag :li, resource_version(proposal_presenter, versions_path: proposal_version_path(@proposal, proposal_presenter.versions.count)), class: "metadata__item" %>

<% fingerprint_id = dom_id(@proposal, :fingerprint_dialog) %>
<%= content_tag :li, class: "metadata__item" do %>
<%= content_tag :button, t("decidim.fingerprint.check"), data: { dialog_open: fingerprint_id } %>
<% end %>
<% fingerprint_id = dom_id(@proposal, :fingerprint_dialog) %>
<%= content_tag :li, class: "metadata__item" do %>
<%= content_tag :button, t("decidim.fingerprint.check"), data: { dialog_open: fingerprint_id } %>
<% end %>
</ul>
<%= decidim_modal id: fingerprint_id, class: "fingerprint-modal" do %>
<div data-dialog-container>
Expand Down
Loading

0 comments on commit b9a4ec9

Please sign in to comment.