From c8c859762b04c5175aa60ca3333a0f17698a779b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E5=BF=83?= <41134017+Lhcfl@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:46:29 +0800 Subject: [PATCH] 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 --- .../components/poll-buttons-dropdown.gjs | 21 ++++++++++ .../components/poll-results-standard.gjs | 15 +++++-- .../components/poll-results-tabs.gjs | 1 + .../javascripts/discourse/components/poll.gjs | 27 ++++++++++++- .../poll/assets/stylesheets/common/poll.scss | 7 +++- plugins/poll/config/locales/client.en.yml | 11 ++++++ .../component/poll-buttons-dropdown-test.js | 39 ++++++++++++++++++- .../component/poll-results-standard-test.js | 37 ++++++++++++++++++ 8 files changed, 150 insertions(+), 8 deletions(-) diff --git a/plugins/poll/assets/javascripts/discourse/components/poll-buttons-dropdown.gjs b/plugins/poll/assets/javascripts/discourse/components/poll-buttons-dropdown.gjs index 06871de3e67..457c4209a7e 100644 --- a/plugins/poll/assets/javascripts/discourse/components/poll-buttons-dropdown.gjs +++ b/plugins/poll/assets/javascripts/discourse/components/poll-buttons-dropdown.gjs @@ -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; diff --git a/plugins/poll/assets/javascripts/discourse/components/poll-results-standard.gjs b/plugins/poll/assets/javascripts/discourse/components/poll-results-standard.gjs index b244563e122..1b0eec3b631 100644 --- a/plugins/poll/assets/javascripts/discourse/components/poll-results-standard.gjs +++ b/plugins/poll/assets/javascripts/discourse/components/poll-results-standard.gjs @@ -76,10 +76,17 @@ export default class PollResultsStandardComponent extends Component {

{{#unless @isRankedChoice}} - {{i18n - "number.percent" - count=option.percentage - }} + {{#if @showTally}} + {{i18n + "poll.votes" + count=option.votes + }} + {{else}} + {{i18n + "number.percent" + count=option.percentage + }} + {{/if}} {{/unless}} {{htmlSafe option.html}}

diff --git a/plugins/poll/assets/javascripts/discourse/components/poll-results-tabs.gjs b/plugins/poll/assets/javascripts/discourse/components/poll-results-tabs.gjs index 67187829b19..c210a4fe9d5 100644 --- a/plugins/poll/assets/javascripts/discourse/components/poll-results-tabs.gjs +++ b/plugins/poll/assets/javascripts/discourse/components/poll-results-tabs.gjs @@ -65,6 +65,7 @@ export default class TabsComponent extends Component { @voters={{@voters}} @votersCount={{@votersCount}} @fetchVoters={{@fetchVoters}} + @showTally={{@showTally}} /> {{/if}} diff --git a/plugins/poll/assets/javascripts/discourse/components/poll.gjs b/plugins/poll/assets/javascripts/discourse/components/poll.gjs index c555c5b7510..6baf80ab27b 100644 --- a/plugins/poll/assets/javascripts/discourse/components/poll.gjs +++ b/plugins/poll/assets/javascripts/discourse/components/poll.gjs @@ -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; + } + diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss index 52fcc440932..188300f28d4 100644 --- a/plugins/poll/assets/stylesheets/common/poll.scss +++ b/plugins/poll/assets/stylesheets/common/poll.scss @@ -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; diff --git a/plugins/poll/config/locales/client.en.yml b/plugins/poll/config/locales/client.en.yml index 850ec5714c2..437a5919f90 100644 --- a/plugins/poll/config/locales/client.en.yml +++ b/plugins/poll/config/locales/client.en.yml @@ -7,6 +7,9 @@ en: total_votes: one: "total vote" other: "total votes" + votes: + one: "%{count} vote" + other: "%{count} votes" average_rating: "Average rating: %{average}." @@ -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" diff --git a/plugins/poll/test/javascripts/component/poll-buttons-dropdown-test.js b/plugins/poll/test/javascripts/component/poll-buttons-dropdown-test.js index c06d523fd84..f48bb87e175 100644 --- a/plugins/poll/test/javascripts/component/poll-buttons-dropdown-test.js +++ b/plugins/poll/test/javascripts/component/poll-buttons-dropdown-test.js @@ -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``); + + 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, diff --git a/plugins/poll/test/javascripts/component/poll-results-standard-test.js b/plugins/poll/test/javascripts/component/poll-results-standard-test.js index 70125f0cea2..7db77e930c3 100644 --- a/plugins/poll/test/javascripts/component/poll-results-standard-test.js +++ b/plugins/poll/test/javascripts/component/poll-results-standard-test.js @@ -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``); + + 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"); + }); });