FEATURE: Absolute Numbers in Poll (#28240)

What does this add?
===================

This PR adds an extra button to the poll to show the absolute number of
people who voted for each option. This button will only be added for
the single/multi-select bar chart.

Related meta topic: https://meta.discourse.org/t/absolute-numbers-in-polls/32771
This commit is contained in:
锦心
2024-08-07 17:46:29 +08:00
committed by GitHub
parent a49a6941c6
commit c8c859762b
8 changed files with 150 additions and 8 deletions

View File

@ -35,6 +35,20 @@ const buttonOptionsMap = {
icon: "lock", icon: "lock",
action: "toggleStatus", action: "toggleStatus",
}, },
showTally: {
className: "btn-default show-tally",
label: "poll.show-tally.label",
title: "poll.show-tally.title",
icon: "info",
action: "toggleDisplayMode",
},
showPercentage: {
className: "btn-default show-percentage",
label: "poll.show-percentage.label",
title: "poll.show-percentage.title",
icon: "info",
action: "toggleDisplayMode",
},
}; };
export default class PollButtonsDropdownComponent extends Component { export default class PollButtonsDropdownComponent extends Component {
@ -68,8 +82,15 @@ export default class PollButtonsDropdownComponent extends Component {
topicArchived, topicArchived,
groupableUserFields, groupableUserFields,
isAutomaticallyClosed, isAutomaticallyClosed,
availableDisplayMode,
} = this.args; } = this.args;
if (availableDisplayMode) {
const option = { ...buttonOptionsMap[availableDisplayMode] };
option.id = option.action;
contents.push(option);
}
if (groupableUserFields.length && voters > 0) { if (groupableUserFields.length && voters > 0) {
const option = { ...buttonOptionsMap.showBreakdown }; const option = { ...buttonOptionsMap.showBreakdown };
option.id = option.action; option.id = option.action;

View File

@ -76,10 +76,17 @@ export default class PollResultsStandardComponent extends Component {
<div class="option"> <div class="option">
<p> <p>
{{#unless @isRankedChoice}} {{#unless @isRankedChoice}}
<span class="percentage">{{i18n {{#if @showTally}}
"number.percent" <span class="absolute">{{i18n
count=option.percentage "poll.votes"
}}</span> count=option.votes
}}</span>
{{else}}
<span class="percentage">{{i18n
"number.percent"
count=option.percentage
}}</span>
{{/if}}
{{/unless}} {{/unless}}
<span class="option-text">{{htmlSafe option.html}}</span> <span class="option-text">{{htmlSafe option.html}}</span>
</p> </p>

View File

@ -65,6 +65,7 @@ export default class TabsComponent extends Component {
@voters={{@voters}} @voters={{@voters}}
@votersCount={{@votersCount}} @votersCount={{@votersCount}}
@fetchVoters={{@fetchVoters}} @fetchVoters={{@fetchVoters}}
@showTally={{@showTally}}
/> />
{{/if}} {{/if}}

View File

@ -12,7 +12,11 @@ import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import PollBreakdownModal from "../components/modal/poll-breakdown"; import PollBreakdownModal from "../components/modal/poll-breakdown";
import { PIE_CHART_TYPE } from "../components/modal/poll-ui-builder"; import {
MULTIPLE_POLL_TYPE,
PIE_CHART_TYPE,
REGULAR_POLL_TYPE,
} from "../components/modal/poll-ui-builder";
import PollButtonsDropdown from "../components/poll-buttons-dropdown"; import PollButtonsDropdown from "../components/poll-buttons-dropdown";
import PollInfo from "../components/poll-info"; import PollInfo from "../components/poll-info";
import PollOptions from "../components/poll-options"; import PollOptions from "../components/poll-options";
@ -48,6 +52,8 @@ export default class PollComponent extends Component {
(this.topicArchived && !this.staffOnly) || (this.topicArchived && !this.staffOnly) ||
(this.closed && !this.staffOnly); (this.closed && !this.staffOnly);
@tracked showTally = false;
checkUserGroups = (user, poll) => { checkUserGroups = (user, poll) => {
const pollGroups = const pollGroups =
poll && poll.groups && poll.groups.split(",").map((g) => g.toLowerCase()); poll && poll.groups && poll.groups.split(",").map((g) => g.toLowerCase());
@ -452,6 +458,17 @@ export default class PollComponent extends Component {
return htmlSafe(I18n.t("poll.average_rating", { average })); return htmlSafe(I18n.t("poll.average_rating", { average }));
} }
get availableDisplayMode() {
if (
!this.showResults ||
this.poll.chart_type === PIE_CHART_TYPE ||
![REGULAR_POLL_TYPE, MULTIPLE_POLL_TYPE].includes(this.poll.type)
) {
return null;
}
return this.showTally ? "showPercentage" : "showTally";
}
@action @action
updatedVoters() { updatedVoters() {
this.preloadedVoters = this.defaultPreloadedVoters(); this.preloadedVoters = this.defaultPreloadedVoters();
@ -640,6 +657,12 @@ export default class PollComponent extends Component {
} }
}); });
} }
@action
toggleDisplayMode() {
this.showTally = !this.showTally;
}
<template> <template>
<div <div
{{didUpdate this.updatedVoters @preloadedVoters}} {{didUpdate this.updatedVoters @preloadedVoters}}
@ -669,6 +692,7 @@ export default class PollComponent extends Component {
@votersCount={{this.poll.voters}} @votersCount={{this.poll.voters}}
@fetchVoters={{this.fetchVoters}} @fetchVoters={{this.fetchVoters}}
@rankedChoiceOutcome={{this.rankedChoiceOutcome}} @rankedChoiceOutcome={{this.rankedChoiceOutcome}}
@showTally={{this.showTally}}
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
@ -754,6 +778,7 @@ export default class PollComponent extends Component {
@groupableUserFields={{this.groupableUserFields}} @groupableUserFields={{this.groupableUserFields}}
@isAutomaticallyClosed={{this.isAutomaticallyClosed}} @isAutomaticallyClosed={{this.isAutomaticallyClosed}}
@dropDownClick={{this.dropDownClick}} @dropDownClick={{this.dropDownClick}}
@availableDisplayMode={{this.availableDisplayMode}}
/> />
</div> </div>
</template> </template>

View File

@ -347,7 +347,9 @@ div.poll-outer {
.poll-buttons-dropdown, .poll-buttons-dropdown,
.export-results, .export-results,
.toggle-status, .toggle-status,
.show-breakdown { .show-breakdown,
.show-tally,
.show-percentage {
// we want these controls to be separated // we want these controls to be separated
// from voting controls // from voting controls
margin-left: auto; margin-left: auto;
@ -367,7 +369,8 @@ div.poll-outer {
} }
} }
.percentage { .percentage,
.absolute {
float: right; float: right;
color: var(--primary-medium); color: var(--primary-medium);
margin-left: 0.25em; margin-left: 0.25em;

View File

@ -7,6 +7,9 @@ en:
total_votes: total_votes:
one: "total vote" one: "total vote"
other: "total votes" other: "total votes"
votes:
one: "%{count} vote"
other: "%{count} votes"
average_rating: "Average rating: <strong>%{average}</strong>." average_rating: "Average rating: <strong>%{average}</strong>."
@ -46,6 +49,14 @@ en:
title: "Display the poll results" title: "Display the poll results"
label: "Results" label: "Results"
show-tally:
title: "Show voting results by number of votes"
label: "Display tally"
show-percentage:
title: "Show voting results as percentage"
label: "Display as percentage"
remove-vote: remove-vote:
title: "Remove your vote" title: "Remove your vote"
label: "Undo vote" label: "Undo vote"

View File

@ -37,7 +37,7 @@ module("Poll | Component | poll-buttons-dropdown", function (hooks) {
await click(".widget-dropdown-header"); await click(".widget-dropdown-header");
assert.strictEqual(count("li.dropdown-menu__item"), 2); assert.dom("li.dropdown-menu__item").exists({ count: 2 });
assert.strictEqual( assert.strictEqual(
query("li.dropdown-menu__item span").textContent.trim(), query("li.dropdown-menu__item span").textContent.trim(),
@ -46,6 +46,43 @@ module("Poll | Component | poll-buttons-dropdown", function (hooks) {
); );
}); });
test("Renders a show-tally button when poll is a bar chart", async function (assert) {
this.setProperties({
closed: false,
voters: 2,
isStaff: false,
isMe: false,
topicArchived: false,
groupableUserFields: ["stuff"],
isAutomaticallyClosed: false,
dropDownClick: () => {},
availableDisplayMode: "showTally",
});
await render(hbs`<PollButtonsDropdown
@closed={{this.closed}}
@voters={{this.voters}}
@isStaff={{this.isStaff}}
@isMe={{this.isMe}}
@topicArchived={{this.topicArchived}}
@groupableUserFields={{this.groupableUserFields}}
@isAutomaticallyClosed={{this.isAutomaticallyClosed}}
@dropDownClick={{this.dropDownClick}}
@availableDisplayMode={{this.availableDisplayMode}}
/>`);
await click(".widget-dropdown-header");
assert.strictEqual(count("li.dropdown-menu__item"), 2);
assert
.dom(query("li.dropdown-menu__item span"))
.hasText(
I18n.t("poll.show-tally.label"),
"displays the show absolute button"
);
});
test("Renders a single button when there is only one authorised action", async function (assert) { test("Renders a single button when there is only one authorised action", async function (assert) {
this.setProperties({ this.setProperties({
closed: false, closed: false,

View File

@ -3,6 +3,7 @@ import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, queryAll } from "discourse/tests/helpers/qunit-helpers"; import { exists, queryAll } from "discourse/tests/helpers/qunit-helpers";
import I18n from "discourse-i18n";
const TWO_OPTIONS = [ const TWO_OPTIONS = [
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 5, rank: 0 }, { id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 5, rank: 0 },
@ -160,4 +161,40 @@ module("Poll | Component | poll-results-standard", function (hooks) {
"b" "b"
); );
}); });
test("options in ascending order, showing absolute vote number", async function (assert) {
this.setProperties({
options: FIVE_OPTIONS,
pollName: "Five Multi Option Poll",
pollType: "multiple",
postId: 123,
vote: ["1ddc47be0d2315b9711ee8526ca9d83f"],
voters: PRELOADEDVOTERS,
votersCount: 12,
fetchVoters: () => {},
showTally: true,
});
await render(hbs`<PollResultsStandard
@options={{this.options}}
@pollName={{this.pollName}}
@pollType={{this.pollType}}
@postId={{this.postId}}
@vote={{this.vote}}
@voters={{this.voters}}
@votersCount={{this.votersCount}}
@fetchVoters={{this.fetchVoters}}
@showTally={{this.showTally}}
/>`);
let percentages = queryAll(".option .absolute");
assert.dom(percentages[0]).hasText(I18n.t("poll.votes", { count: 5 }));
assert.dom(percentages[1]).hasText(I18n.t("poll.votes", { count: 4 }));
assert.dom(percentages[2]).hasText(I18n.t("poll.votes", { count: 2 }));
assert.dom(percentages[3]).hasText(I18n.t("poll.votes", { count: 1 }));
assert.dom(queryAll(".option")[3].querySelectorAll("span")[1]).hasText("a");
assert.dom(percentages[4]).hasText(I18n.t("poll.votes", { count: 1 }));
assert.dom(queryAll(".option")[4].querySelectorAll("span")[1]).hasText("b");
});
}); });