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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 150 additions and 8 deletions

View File

@ -35,6 +35,20 @@ const buttonOptionsMap = {
icon: "lock",
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 {
@ -68,8 +82,15 @@ export default class PollButtonsDropdownComponent extends Component {
topicArchived,
groupableUserFields,
isAutomaticallyClosed,
availableDisplayMode,
} = this.args;
if (availableDisplayMode) {
const option = { ...buttonOptionsMap[availableDisplayMode] };
option.id = option.action;
contents.push(option);
}
if (groupableUserFields.length && voters > 0) {
const option = { ...buttonOptionsMap.showBreakdown };
option.id = option.action;

View File

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

View File

@ -65,6 +65,7 @@ export default class TabsComponent extends Component {
@voters={{@voters}}
@votersCount={{@votersCount}}
@fetchVoters={{@fetchVoters}}
@showTally={{@showTally}}
/>
{{/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-i18n";
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 PollInfo from "../components/poll-info";
import PollOptions from "../components/poll-options";
@ -48,6 +52,8 @@ export default class PollComponent extends Component {
(this.topicArchived && !this.staffOnly) ||
(this.closed && !this.staffOnly);
@tracked showTally = false;
checkUserGroups = (user, poll) => {
const pollGroups =
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 }));
}
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
updatedVoters() {
this.preloadedVoters = this.defaultPreloadedVoters();
@ -640,6 +657,12 @@ export default class PollComponent extends Component {
}
});
}
@action
toggleDisplayMode() {
this.showTally = !this.showTally;
}
<template>
<div
{{didUpdate this.updatedVoters @preloadedVoters}}
@ -669,6 +692,7 @@ export default class PollComponent extends Component {
@votersCount={{this.poll.voters}}
@fetchVoters={{this.fetchVoters}}
@rankedChoiceOutcome={{this.rankedChoiceOutcome}}
@showTally={{this.showTally}}
/>
{{/if}}
{{/if}}
@ -754,6 +778,7 @@ export default class PollComponent extends Component {
@groupableUserFields={{this.groupableUserFields}}
@isAutomaticallyClosed={{this.isAutomaticallyClosed}}
@dropDownClick={{this.dropDownClick}}
@availableDisplayMode={{this.availableDisplayMode}}
/>
</div>
</template>

View File

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

View File

@ -7,6 +7,9 @@ en:
total_votes:
one: "total vote"
other: "total votes"
votes:
one: "%{count} vote"
other: "%{count} votes"
average_rating: "Average rating: <strong>%{average}</strong>."
@ -46,6 +49,14 @@ en:
title: "Display the poll 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:
title: "Remove your vote"
label: "Undo vote"

View File

@ -37,7 +37,7 @@ module("Poll | Component | poll-buttons-dropdown", function (hooks) {
await click(".widget-dropdown-header");
assert.strictEqual(count("li.dropdown-menu__item"), 2);
assert.dom("li.dropdown-menu__item").exists({ count: 2 });
assert.strictEqual(
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) {
this.setProperties({
closed: false,

View File

@ -3,6 +3,7 @@ import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, queryAll } from "discourse/tests/helpers/qunit-helpers";
import I18n from "discourse-i18n";
const TWO_OPTIONS = [
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 5, rank: 0 },
@ -160,4 +161,40 @@ module("Poll | Component | poll-results-standard", function (hooks) {
"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");
});
});