FEATURE: Add Instant Run-off Voting to Poll Plugin (Part 1: migrate existing plugin to Glimmer only) (#27204)
The "migration to Glimmer" has been broken out here from #27155 to make the review process less onerous and reduce change risk: * DEV: migrates most of the widget code to Glimmer in prep for IRV additions * NB This already incorporates significant amounts of review and feedback from the prior PR. * NB because there was significant additional feedback relating to older Poll code that I've improved with feedback, there are some additional changes here that are general improvements to the plugin and not specific to IRV nor Glimmer! * There should be no trace of IRV code here. Once this is finalised and merged we can continue to progress with #27155.
This commit is contained in:
parent
32c8bcc3af
commit
e3b6be15b8
|
@ -17,6 +17,7 @@ en:
|
|||
kb: KB
|
||||
mb: MB
|
||||
tb: TB
|
||||
percent: "%{count}%"
|
||||
short:
|
||||
thousands: "%{number}k"
|
||||
millions: "%{number}M"
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PollSerializer < ApplicationSerializer
|
||||
attributes :name,
|
||||
attributes :id,
|
||||
:name,
|
||||
:type,
|
||||
:status,
|
||||
:public,
|
||||
|
|
|
@ -173,4 +173,9 @@ export default class PollBreakdownChart extends Component {
|
|||
this._chart.setActiveElements(activeElements);
|
||||
this._chart.update();
|
||||
}
|
||||
|
||||
<template>
|
||||
<label class="poll-breakdown-chart-label">{{@group}}</label>
|
||||
<canvas class="poll-breakdown-chart-chart"></canvas>
|
||||
</template>
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<label class="poll-breakdown-chart-label">{{@group}}</label>
|
||||
<canvas class="poll-breakdown-chart-chart"></canvas>
|
|
@ -1,8 +1,10 @@
|
|||
import Component from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
import { propertyEqual } from "discourse/lib/computed";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import { getColors } from "discourse/plugins/poll/lib/chart-colors";
|
||||
|
@ -48,4 +50,28 @@ export default class PollBreakdownOption extends Component {
|
|||
|
||||
return htmlSafe(`background: ${color};`);
|
||||
}
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="poll-breakdown-option"
|
||||
style={{this.colorBackgroundStyle}}
|
||||
{{on "mouseover" @onMouseOver}}
|
||||
{{on "mouseout" @onMouseOut}}
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="poll-breakdown-option-color"
|
||||
style={{this.colorPreviewStyle}}
|
||||
></span>
|
||||
|
||||
<span class="poll-breakdown-option-count">
|
||||
{{#if this.showPercentage}}
|
||||
{{i18n "number.percent" count=this.percent}}
|
||||
{{else}}
|
||||
{{@option.votes}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="poll-breakdown-option-text">{{htmlSafe @option.html}}</span>
|
||||
</li>
|
||||
</template>
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<li
|
||||
class="poll-breakdown-option"
|
||||
style={{this.colorBackgroundStyle}}
|
||||
{{on "mouseover" @onMouseOver}}
|
||||
{{on "mouseout" @onMouseOut}}
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="poll-breakdown-option-color"
|
||||
style={{this.colorPreviewStyle}}
|
||||
></span>
|
||||
|
||||
<span class="poll-breakdown-option-count">
|
||||
{{#if this.showPercentage}}
|
||||
{{this.percent}}%
|
||||
{{else}}
|
||||
{{@option.votes}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="poll-breakdown-option-text">{{html-safe @option.html}}</span>
|
||||
</li>
|
|
@ -0,0 +1,132 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
const buttonOptionsMap = {
|
||||
exportResults: {
|
||||
className: "btn-default export-results",
|
||||
label: "poll.export-results.label",
|
||||
title: "poll.export-results.title",
|
||||
icon: "download",
|
||||
action: "exportResults",
|
||||
},
|
||||
showBreakdown: {
|
||||
className: "btn-default show-breakdown",
|
||||
label: "poll.breakdown.breakdown",
|
||||
icon: "chart-pie",
|
||||
action: "showBreakdown",
|
||||
},
|
||||
openPoll: {
|
||||
className: "btn-default toggle-status",
|
||||
label: "poll.open.label",
|
||||
title: "poll.open.title",
|
||||
icon: "unlock-alt",
|
||||
action: "toggleStatus",
|
||||
},
|
||||
closePoll: {
|
||||
className: "btn-default toggle-status",
|
||||
label: "poll.close.label",
|
||||
title: "poll.close.title",
|
||||
icon: "lock",
|
||||
action: "toggleStatus",
|
||||
},
|
||||
};
|
||||
|
||||
export default class PollButtonsDropdownComponent extends Component {
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.getDropdownButtonState = false;
|
||||
}
|
||||
|
||||
@action
|
||||
dropDownClick(dropDownAction) {
|
||||
this.args.dropDownClick(dropDownAction);
|
||||
}
|
||||
|
||||
get getDropdownContent() {
|
||||
const contents = [];
|
||||
const isAdmin = this.currentUser && this.currentUser.admin;
|
||||
|
||||
const dataExplorerEnabled = this.siteSettings.data_explorer_enabled;
|
||||
const exportQueryID = this.siteSettings.poll_export_data_explorer_query_id;
|
||||
|
||||
const {
|
||||
closed,
|
||||
voters,
|
||||
isStaff,
|
||||
isMe,
|
||||
topicArchived,
|
||||
groupableUserFields,
|
||||
isAutomaticallyClosed,
|
||||
} = this.args;
|
||||
|
||||
if (groupableUserFields.length && voters > 0) {
|
||||
const option = { ...buttonOptionsMap.showBreakdown };
|
||||
option.id = option.action;
|
||||
contents.push(option);
|
||||
}
|
||||
|
||||
if (isAdmin && dataExplorerEnabled && voters > 0 && exportQueryID) {
|
||||
const option = { ...buttonOptionsMap.exportResults };
|
||||
option.id = option.action;
|
||||
contents.push(option);
|
||||
}
|
||||
|
||||
if (this.currentUser && (isMe || isStaff) && !topicArchived) {
|
||||
if (closed) {
|
||||
if (!isAutomaticallyClosed) {
|
||||
const option = { ...buttonOptionsMap.openPoll };
|
||||
option.id = option.action;
|
||||
contents.push(option);
|
||||
}
|
||||
} else {
|
||||
const option = { ...buttonOptionsMap.closePoll };
|
||||
option.id = option.action;
|
||||
contents.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
get showDropdown() {
|
||||
return this.getDropdownContent.length > 1;
|
||||
}
|
||||
|
||||
get showDropdownAsButton() {
|
||||
return this.getDropdownContent.length === 1;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="poll-buttons-dropdown">
|
||||
<DMenu class="widget-dropdown-header">
|
||||
<:trigger>
|
||||
{{icon "cog"}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#each this.getDropdownContent as |content|}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
class="widget-button {{content.className}}"
|
||||
@icon={{content.icon}}
|
||||
@label={{content.label}}
|
||||
@action={{fn this.dropDownClick content.action}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
<dropdown.divider />
|
||||
{{/each}}
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { relativeAge } from "discourse/lib/formatter";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import I18n from "I18n";
|
||||
|
||||
const ON_VOTE = "on_vote";
|
||||
const ON_CLOSE = "on_close";
|
||||
const STAFF_ONLY = "staff_only";
|
||||
|
||||
export default class PollInfoComponent extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get multipleHelpText() {
|
||||
const { min, max, options } = this.args;
|
||||
const optionsCount = options.length;
|
||||
|
||||
if (max > 0) {
|
||||
if (min === max && min > 1) {
|
||||
return htmlSafe(I18n.t("poll.multiple.help.x_options", { count: min }));
|
||||
}
|
||||
|
||||
if (min > 1) {
|
||||
if (max < optionsCount) {
|
||||
return htmlSafe(
|
||||
I18n.t("poll.multiple.help.between_min_and_max_options", {
|
||||
min,
|
||||
max,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return htmlSafe(
|
||||
I18n.t("poll.multiple.help.at_least_min_options", { count: min })
|
||||
);
|
||||
}
|
||||
|
||||
if (max <= optionsCount) {
|
||||
return htmlSafe(
|
||||
I18n.t("poll.multiple.help.up_to_max_options", { count: max })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get votersLabel() {
|
||||
return I18n.t("poll.voters", { count: this.args.voters });
|
||||
}
|
||||
|
||||
get showTotalVotes() {
|
||||
return this.args.isMultiple && (this.args.showResults || this.args.closed);
|
||||
}
|
||||
|
||||
get totalVotes() {
|
||||
return this.args.options.reduce((total, o) => {
|
||||
return total + parseInt(o.votes, 10);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
get totalVotesLabel() {
|
||||
return I18n.t("poll.total_votes", { count: this.totalVotes });
|
||||
}
|
||||
|
||||
get automaticCloseAgeLabel() {
|
||||
return I18n.t("poll.automatic_close.age", this.age);
|
||||
}
|
||||
|
||||
get automaticCloseClosesInLabel() {
|
||||
return I18n.t("poll.automatic_close.closes_in", this.timeLeft);
|
||||
}
|
||||
|
||||
get showMultipleHelpText() {
|
||||
return this.args.isMultiple && !this.args.showResults && !this.args.closed;
|
||||
}
|
||||
|
||||
get closeTitle() {
|
||||
const closeDate = moment.utc(this.args.close, "YYYY-MM-DD HH:mm:ss Z");
|
||||
if (closeDate.isValid()) {
|
||||
return closeDate.format("LLL");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
get age() {
|
||||
const closeDate = moment.utc(this.args.close, "YYYY-MM-DD HH:mm:ss Z");
|
||||
if (closeDate.isValid()) {
|
||||
return relativeAge(closeDate.toDate(), { addAgo: true });
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
get timeLeft() {
|
||||
const closeDate = moment.utc(this.args.close, "YYYY-MM-DD HH:mm:ss Z");
|
||||
if (closeDate.isValid()) {
|
||||
return moment().to(closeDate, true);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
get resultsOnVote() {
|
||||
return (
|
||||
this.args.results === ON_VOTE &&
|
||||
!this.args.hasVoted &&
|
||||
!(this.currentUser && this.args.postUserId === this.currentUser.id)
|
||||
);
|
||||
}
|
||||
|
||||
get resultsOnClose() {
|
||||
return this.args.results === ON_CLOSE && !this.args.closed;
|
||||
}
|
||||
|
||||
get resultsStaffOnly() {
|
||||
return (
|
||||
this.args.results === STAFF_ONLY &&
|
||||
!(this.currentUser && this.currentUser.staff)
|
||||
);
|
||||
}
|
||||
|
||||
get publicTitle() {
|
||||
return (
|
||||
!this.args.closed &&
|
||||
!this.args.showResults &&
|
||||
this.args.isPublic &&
|
||||
this.args.results !== STAFF_ONLY
|
||||
);
|
||||
}
|
||||
|
||||
get publicTitleLabel() {
|
||||
return htmlSafe(I18n.t("poll.public.title"));
|
||||
}
|
||||
|
||||
get showInstructionsSection() {
|
||||
return (
|
||||
this.showMultipleHelpText ||
|
||||
this.args.close ||
|
||||
this.resultsOnVote ||
|
||||
this.resultsOnClose ||
|
||||
this.resultsStaffOnly ||
|
||||
this.publicTitle
|
||||
);
|
||||
}
|
||||
<template>
|
||||
<div class="poll-info">
|
||||
<div class="poll-info_counts">
|
||||
<div class="poll-info_counts-count">
|
||||
<span class="info-number">{{@voters}}</span>
|
||||
<span class="info-label">{{this.votersLabel}}</span>
|
||||
</div>
|
||||
{{#if this.showTotalVotes}}
|
||||
<div class="poll-info_counts-count">
|
||||
<span class="info-number">{{this.totalVotes}}</span>
|
||||
<span class="info-label">{{this.totalVotesLabel}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if this.showInstructionsSection}}
|
||||
<ul class="poll-info_instructions">
|
||||
{{#if this.showMultipleHelpText}}
|
||||
<li class="multiple-help-text">
|
||||
{{icon "list-ul"}}
|
||||
<span>{{this.multipleHelpText}}</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if this.poll.close}}
|
||||
{{#if this.isAutomaticallyClosed}}
|
||||
<li title={{this.title}}>
|
||||
{{icon "lock"}}
|
||||
<span>{{this.automaticCloseAgeLabel}}</span>
|
||||
</li>
|
||||
{{else}}
|
||||
<li title={{this.title}}>
|
||||
{{icon "far-clock"}}
|
||||
<span>{{this.automaticCloseClosesInLabel}}</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if this.resultsOnVote}}
|
||||
<li>
|
||||
{{icon "check"}}
|
||||
<span>{{I18n "poll.results.vote.title"}}</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if this.resultsOnClose}}
|
||||
<li>
|
||||
{{icon "lock"}}
|
||||
<span>{{I18n "poll.results.closed.title"}}</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if this.resultsStaffOnly}}
|
||||
<li>
|
||||
{{icon "shield-alt"}}
|
||||
<span>{{I18n "poll.results.staff.title"}}</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if this.publicTitle}}
|
||||
<li class="is-public">
|
||||
{{icon "far-eye"}}
|
||||
<span>{{this.publicTitleLabel}}</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
|
||||
export default class PollOptionsComponent extends Component {
|
||||
@service currentUser;
|
||||
|
||||
isChosen = (option) => {
|
||||
return this.args.votes.includes(option.id);
|
||||
};
|
||||
|
||||
@action
|
||||
sendClick(option) {
|
||||
this.args.sendOptionSelect(option);
|
||||
}
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
{{#each @options as |option|}}
|
||||
<li tabindex="0" data-poll-option-id={{option.id}}>
|
||||
{{#if this.currentUser}}
|
||||
<button {{on "click" (fn this.sendClick option)}}>
|
||||
{{#if (this.isChosen option)}}
|
||||
{{#if @isCheckbox}}
|
||||
{{icon "far-check-square"}}
|
||||
{{else}}
|
||||
{{icon "circle"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if @isCheckbox}}
|
||||
{{icon "far-square"}}
|
||||
{{else}}
|
||||
{{icon "far-circle"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<span class="option-text">{{option.html}}</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button onclick={{routeAction "showLogin"}}>
|
||||
{{#if (this.isChosen option)}}
|
||||
{{#if @isCheckbox}}
|
||||
{{icon "far-check-square"}}
|
||||
{{else}}
|
||||
{{icon "circle"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if @isCheckbox}}
|
||||
{{icon "far-square"}}
|
||||
{{else}}
|
||||
{{icon "far-circle"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<span class="option-text">{{option.html}}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { modifier } from "ember-modifier";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { getColors } from "discourse/plugins/poll/lib/chart-colors";
|
||||
import { PIE_CHART_TYPE } from "../components/modal/poll-ui-builder";
|
||||
|
||||
export default class PollResultsPieComponent extends Component {
|
||||
htmlLegendPlugin = {
|
||||
id: "htmlLegend",
|
||||
|
||||
afterUpdate(chart, args, options) {
|
||||
const ul = document.getElementById(options.containerID);
|
||||
ul.innerHTML = "";
|
||||
|
||||
const items = chart.options.plugins.legend.labels.generateLabels(chart);
|
||||
items.forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
li.classList.add("legend");
|
||||
li.onclick = () => {
|
||||
chart.toggleDataVisibility(item.index);
|
||||
chart.update();
|
||||
};
|
||||
|
||||
const boxSpan = document.createElement("span");
|
||||
boxSpan.classList.add("swatch");
|
||||
boxSpan.style.background = item.fillStyle;
|
||||
|
||||
const textContainer = document.createElement("span");
|
||||
textContainer.style.color = item.fontColor;
|
||||
textContainer.innerHTML = item.text;
|
||||
|
||||
if (!chart.getDataVisibility(item.index)) {
|
||||
li.style.opacity = 0.2;
|
||||
} else {
|
||||
li.style.opacity = 1.0;
|
||||
}
|
||||
|
||||
li.appendChild(boxSpan);
|
||||
li.appendChild(textContainer);
|
||||
|
||||
ul.appendChild(li);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
stripHtml = (html) => {
|
||||
let doc = new DOMParser().parseFromString(html, "text/html");
|
||||
return doc.body.textContent || "";
|
||||
};
|
||||
|
||||
pieChartConfig = (data, labels, opts = {}) => {
|
||||
const aspectRatio = "aspectRatio" in opts ? opts.aspectRatio : 2.2;
|
||||
const strippedLabels = labels.map((l) => this.stripHtml(l));
|
||||
|
||||
return {
|
||||
type: PIE_CHART_TYPE,
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: getColors(data.length),
|
||||
},
|
||||
],
|
||||
labels: strippedLabels,
|
||||
},
|
||||
plugins: [this.htmlLegendPlugin],
|
||||
options: {
|
||||
responsive: true,
|
||||
aspectRatio,
|
||||
animation: { duration: 0 },
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
generateLabels() {
|
||||
return labels.map((text, index) => {
|
||||
return {
|
||||
fillStyle: getColors(data.length)[index],
|
||||
text,
|
||||
index,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
htmlLegend: {
|
||||
containerID: opts?.legendContainerId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
registerLegendElement = modifier((element) => {
|
||||
this.legendElement = element;
|
||||
});
|
||||
registerCanvasElement = modifier((element) => {
|
||||
this.canvasElement = element;
|
||||
});
|
||||
get canvasId() {
|
||||
return htmlSafe(`poll-results-chart-${this.args.id}`);
|
||||
}
|
||||
|
||||
get legendId() {
|
||||
return htmlSafe(`poll-results-legend-${this.args.id}`);
|
||||
}
|
||||
|
||||
@action
|
||||
async drawPie() {
|
||||
await loadScript("/javascripts/Chart.min.js");
|
||||
|
||||
const data = this.args.options.mapBy("votes");
|
||||
const labels = this.args.options.mapBy("html");
|
||||
const config = this.pieChartConfig(data, labels, {
|
||||
legendContainerId: this.legendElement.id,
|
||||
});
|
||||
const el = this.canvasElement;
|
||||
// eslint-disable-next-line no-undef
|
||||
this._chart = new Chart(el.getContext("2d"), config);
|
||||
}
|
||||
<template>
|
||||
<div class="poll-results-chart">
|
||||
<canvas
|
||||
{{didInsert this.drawPie}}
|
||||
{{didInsert this.registerCanvasElement}}
|
||||
id={{this.canvasId}}
|
||||
class="poll-results-canvas"
|
||||
></canvas>
|
||||
<ul
|
||||
{{didInsert this.registerLegendElement}}
|
||||
id={{this.legendId}}
|
||||
class="pie-chart-legends"
|
||||
>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import evenRound from "discourse/plugins/poll/lib/even-round";
|
||||
import PollVoters from "./poll-voters";
|
||||
|
||||
export default class PollResultsStandardComponent extends Component {
|
||||
orderOptions = (options) => {
|
||||
return options.sort((a, b) => {
|
||||
if (a.votes < b.votes) {
|
||||
return 1;
|
||||
} else if (a.votes === b.votes) {
|
||||
if (a.html < b.html) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getPercentages = (ordered, votersCount) => {
|
||||
return votersCount === 0
|
||||
? Array(ordered.length).fill(0)
|
||||
: ordered.map((o) => (100 * o.votes) / votersCount);
|
||||
};
|
||||
|
||||
roundPercentages = (percentages) => {
|
||||
return this.isMultiple
|
||||
? percentages.map(Math.floor)
|
||||
: evenRound(percentages);
|
||||
};
|
||||
|
||||
enrichOptions = (ordered, rounded) => {
|
||||
ordered.forEach((option, idx) => {
|
||||
const per = rounded[idx].toString();
|
||||
const chosen = (this.args.vote || []).includes(option.id);
|
||||
option.percentage = per;
|
||||
option.chosen = chosen;
|
||||
let voters = this.args.voters[option.id] || [];
|
||||
option.voters = [...voters];
|
||||
});
|
||||
|
||||
return ordered;
|
||||
};
|
||||
|
||||
get votersCount() {
|
||||
return this.args.votersCount || 0;
|
||||
}
|
||||
|
||||
get orderedOptions() {
|
||||
const ordered = this.orderOptions([...this.args.options]);
|
||||
|
||||
const percentages = this.getPercentages(ordered, this.votersCount);
|
||||
|
||||
const roundedPercentages = this.roundPercentages(percentages);
|
||||
|
||||
return this.enrichOptions(ordered, roundedPercentages);
|
||||
}
|
||||
|
||||
get isMultiple() {
|
||||
return this.args.pollType === "multiple";
|
||||
}
|
||||
<template>
|
||||
<ul class="results">
|
||||
{{#each this.orderedOptions key="voters" as |option|}}
|
||||
<li class={{if option.chosen "chosen" ""}}>
|
||||
<div class="option">
|
||||
<p>
|
||||
<span class="percentage">{{i18n
|
||||
"number.percent"
|
||||
count=option.percentage
|
||||
}}</span>
|
||||
<span class="option-text">{{option.html}}</span>
|
||||
</p>
|
||||
<div class="bar-back">
|
||||
<div
|
||||
class="bar"
|
||||
style={{htmlSafe (concat "width:" option.percentage "%")}}
|
||||
/>
|
||||
</div>
|
||||
<PollVoters
|
||||
@postId={{@postId}}
|
||||
@pollType={{@pollType}}
|
||||
@optionId={{option.id}}
|
||||
@pollName={{@pollName}}
|
||||
@totalVotes={{option.votes}}
|
||||
@voters={{option.voters}}
|
||||
@fetchVoters={{@fetchVoters}}
|
||||
@loading={{option.loading}}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import avatar from "discourse/helpers/bound-avatar-template";
|
||||
|
||||
export default class PollVotersComponent extends Component {
|
||||
get showMore() {
|
||||
return this.args.voters.length < this.args.totalVotes;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="poll-voters">
|
||||
<ul class="poll-voters-list">
|
||||
{{#each @voters as |user|}}
|
||||
<li>
|
||||
{{avatar user.avatar_template "tiny"}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if this.showMore}}
|
||||
<ConditionalLoadingSpinner @condition={{@loading}}>
|
||||
<DButton
|
||||
@action={{fn @fetchVoters @optionId}}
|
||||
@icon="chevron-down"
|
||||
class="poll-voters-toggle-expand"
|
||||
/>
|
||||
</ConditionalLoadingSpinner>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,609 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import round from "discourse/lib/round";
|
||||
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 PollButtonsDropdown from "../components/poll-buttons-dropdown";
|
||||
import PollInfo from "../components/poll-info";
|
||||
import PollOptions from "../components/poll-options";
|
||||
import PollResultsPie from "../components/poll-results-pie";
|
||||
import PollResultsStandard from "../components/poll-results-standard";
|
||||
|
||||
const FETCH_VOTERS_COUNT = 25;
|
||||
const STAFF_ONLY = "staff_only";
|
||||
const MULTIPLE = "multiple";
|
||||
const NUMBER = "number";
|
||||
const REGULAR = "regular";
|
||||
const ON_VOTE = "on_vote";
|
||||
const ON_CLOSE = "on_close";
|
||||
|
||||
export default class PollComponent extends Component {
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
@service appEvents;
|
||||
@service dialog;
|
||||
@service router;
|
||||
@service modal;
|
||||
@tracked isStaff = this.currentUser && this.currentUser.staff;
|
||||
@tracked vote = this.args.attrs.vote || [];
|
||||
@tracked titleHTML = htmlSafe(this.args.attrs.titleHTML);
|
||||
@tracked topicArchived = this.args.attrs.post.get("topic.archived");
|
||||
@tracked options = [];
|
||||
@tracked poll = this.args.attrs.poll;
|
||||
@tracked voters = this.poll.voters || 0;
|
||||
@tracked preloadedVoters = this.args.preloadedVoters || [];
|
||||
@tracked staffOnly = this.poll.results === STAFF_ONLY;
|
||||
@tracked isMultiple = this.poll.type === MULTIPLE;
|
||||
@tracked isNumber = this.poll.type === NUMBER;
|
||||
@tracked showingResults = false;
|
||||
@tracked hasSavedVote = this.args.attrs.hasSavedVote;
|
||||
@tracked status = this.poll.status;
|
||||
@tracked
|
||||
showResults =
|
||||
this.hasSavedVote ||
|
||||
this.showingResults ||
|
||||
(this.topicArchived && !this.staffOnly) ||
|
||||
(this.closed && !this.staffOnly);
|
||||
post = this.args.attrs.post;
|
||||
isMe =
|
||||
this.currentUser && this.args.attrs.post.user_id === this.currentUser.id;
|
||||
|
||||
checkUserGroups = (user, poll) => {
|
||||
const pollGroups =
|
||||
poll && poll.groups && poll.groups.split(",").map((g) => g.toLowerCase());
|
||||
|
||||
if (!pollGroups) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userGroups =
|
||||
user && user.groups && user.groups.map((g) => g.name.toLowerCase());
|
||||
|
||||
return userGroups && pollGroups.some((g) => userGroups.includes(g));
|
||||
};
|
||||
castVotes = (option) => {
|
||||
if (!this.canCastVotes) {
|
||||
return;
|
||||
}
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ajax("/polls/vote", {
|
||||
type: "PUT",
|
||||
data: {
|
||||
post_id: this.args.attrs.post.id,
|
||||
poll_name: this.poll.name,
|
||||
options: this.vote,
|
||||
},
|
||||
})
|
||||
.then(({ poll }) => {
|
||||
this.options = [...poll.options];
|
||||
this.hasSavedVote = true;
|
||||
this.poll.setProperties(poll);
|
||||
this.appEvents.trigger(
|
||||
"poll:voted",
|
||||
poll,
|
||||
this.args.attrs.post,
|
||||
this.args.attrs.vote
|
||||
);
|
||||
|
||||
const voters = poll.voters;
|
||||
this.voters = [Number(voters)][0];
|
||||
|
||||
if (this.poll.results !== "on_close") {
|
||||
this.showResults = true;
|
||||
}
|
||||
if (this.poll.results === "staff_only") {
|
||||
if (this.currentUser && this.currentUser.staff) {
|
||||
this.showResults = true;
|
||||
} else {
|
||||
this.showResults = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error) {
|
||||
if (!this.isMultiple) {
|
||||
this._toggleOption(option);
|
||||
}
|
||||
popupAjaxError(error);
|
||||
} else {
|
||||
this.dialog.alert(I18n.t("poll.error_while_casting_votes"));
|
||||
}
|
||||
});
|
||||
};
|
||||
_toggleOption = (option) => {
|
||||
let options = this.options;
|
||||
let vote = this.vote;
|
||||
|
||||
if (this.isMultiple) {
|
||||
const chosenIdx = vote.indexOf(option.id);
|
||||
|
||||
if (chosenIdx !== -1) {
|
||||
vote.splice(chosenIdx, 1);
|
||||
} else {
|
||||
vote.push(option.id);
|
||||
}
|
||||
} else {
|
||||
vote = [option.id];
|
||||
}
|
||||
|
||||
this.vote = [...vote];
|
||||
this.options = [...options];
|
||||
};
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.id = this.args.attrs.id;
|
||||
this.post = this.args.attrs.post;
|
||||
this.options = this.poll.options;
|
||||
this.groupableUserFields = this.args.attrs.groupableUserFields;
|
||||
}
|
||||
|
||||
get min() {
|
||||
let min = parseInt(this.args.attrs.poll.min, 10);
|
||||
if (isNaN(min) || min < 0) {
|
||||
min = 1;
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
get max() {
|
||||
let max = parseInt(this.args.attrs.poll.max, 10);
|
||||
const numOptions = this.args.attrs.poll.options.length;
|
||||
if (isNaN(max) || max > numOptions) {
|
||||
max = numOptions;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
get closed() {
|
||||
return this.status === "closed" || this.isAutomaticallyClosed;
|
||||
}
|
||||
|
||||
get isAutomaticallyClosed() {
|
||||
const poll = this.poll;
|
||||
return (
|
||||
(poll.close ?? false) &&
|
||||
moment.utc(poll.close, "YYYY-MM-DD HH:mm:ss Z") <= moment()
|
||||
);
|
||||
}
|
||||
|
||||
get hasVoted() {
|
||||
return this.vote && this.vote.length > 0;
|
||||
}
|
||||
|
||||
get hideResultsDisabled() {
|
||||
return !this.staffOnly && (this.closed || this.topicArchived);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleOption(option) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
if (!this.currentUser) {
|
||||
// unlikely, handled by template logic
|
||||
return;
|
||||
}
|
||||
if (!this.checkUserGroups(this.currentUser, this.poll)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.isMultiple &&
|
||||
this.vote.length === 1 &&
|
||||
this.vote[0] === option.id
|
||||
) {
|
||||
return this.removeVote();
|
||||
}
|
||||
|
||||
if (!this.isMultiple) {
|
||||
this.vote.length = 0;
|
||||
}
|
||||
|
||||
this._toggleOption(option);
|
||||
|
||||
if (!this.isMultiple) {
|
||||
this.castVotes(option);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleResults() {
|
||||
const showResults = !this.showResults;
|
||||
this.showResults = showResults;
|
||||
}
|
||||
|
||||
get canCastVotes() {
|
||||
if (this.closed || this.showingResults || !this.currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedOptionCount = this.vote?.length || 0;
|
||||
|
||||
if (this.isMultiple) {
|
||||
return selectedOptionCount >= this.min && selectedOptionCount <= this.max;
|
||||
}
|
||||
|
||||
return selectedOptionCount > 0;
|
||||
}
|
||||
|
||||
get notInVotingGroup() {
|
||||
return !this.checkUserGroups(this.currentUser, this.poll);
|
||||
}
|
||||
|
||||
get pollGroups() {
|
||||
return I18n.t("poll.results.groups.title", { groups: this.poll.groups });
|
||||
}
|
||||
|
||||
get showCastVotesButton() {
|
||||
return this.isMultiple && !this.showResults;
|
||||
}
|
||||
|
||||
get castVotesButtonClass() {
|
||||
return `btn cast-votes ${
|
||||
this.canCastVotes ? "btn-primary" : "btn-default"
|
||||
}`;
|
||||
}
|
||||
|
||||
get castVotesButtonIcon() {
|
||||
return !this.castVotesDisabled ? "check" : "far-square";
|
||||
}
|
||||
|
||||
get castVotesDisabled() {
|
||||
return !this.canCastVotes;
|
||||
}
|
||||
|
||||
get showHideResultsButton() {
|
||||
return this.showResults && !this.hideResultsDisabled;
|
||||
}
|
||||
|
||||
get showShowResultsButton() {
|
||||
return (
|
||||
!this.showResults &&
|
||||
!this.hideResultsDisabled &&
|
||||
!(this.poll.results === ON_VOTE && !this.hasSavedVote && !this.isMe) &&
|
||||
!(this.poll.results === ON_CLOSE && !this.closed) &&
|
||||
!(this.poll.results === STAFF_ONLY && !this.isStaff) &&
|
||||
this.voters > 0
|
||||
);
|
||||
}
|
||||
|
||||
get showRemoveVoteButton() {
|
||||
return (
|
||||
!this.showResults &&
|
||||
!this.closed &&
|
||||
!this.hideResultsDisabled &&
|
||||
this.hasSavedVote
|
||||
);
|
||||
}
|
||||
|
||||
get isCheckbox() {
|
||||
if (this.isMultiple) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get resultsWidgetTypeClass() {
|
||||
const type = this.poll.type;
|
||||
return this.isNumber || this.poll.chart_type !== PIE_CHART_TYPE
|
||||
? `discourse-poll-${type}-results`
|
||||
: "discourse-poll-pie-chart";
|
||||
}
|
||||
|
||||
get resultsPie() {
|
||||
return this.poll.chart_type === PIE_CHART_TYPE;
|
||||
}
|
||||
|
||||
get averageRating() {
|
||||
const totalScore = this.options.reduce((total, o) => {
|
||||
return total + parseInt(o.html, 10) * parseInt(o.votes, 10);
|
||||
}, 0);
|
||||
|
||||
const average = this.voters === 0 ? 0 : round(totalScore / this.voters, -2);
|
||||
|
||||
return htmlSafe(I18n.t("poll.average_rating", { average }));
|
||||
}
|
||||
|
||||
@action
|
||||
updatedVoters() {
|
||||
this.preloadedVoters = this.args.preloadedVoters;
|
||||
this.options = [...this.args.options];
|
||||
}
|
||||
|
||||
@action
|
||||
fetchVoters(optionId) {
|
||||
let votersCount;
|
||||
this.loading = true;
|
||||
let options = this.options;
|
||||
options.find((option) => option.id === optionId).loading = true;
|
||||
this.options = [...options];
|
||||
|
||||
votersCount = this.options.find((option) => option.id === optionId).votes;
|
||||
|
||||
return ajax("/polls/voters.json", {
|
||||
data: {
|
||||
post_id: this.post.id,
|
||||
poll_name: this.poll.name,
|
||||
option_id: optionId,
|
||||
page: Math.floor(votersCount / FETCH_VOTERS_COUNT) + 1,
|
||||
limit: FETCH_VOTERS_COUNT,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const voters = optionId
|
||||
? this.preloadedVoters[optionId]
|
||||
: this.preloadedVoters;
|
||||
const newVoters = optionId ? result.voters[optionId] : result.voters;
|
||||
const votersSet = new Set(voters.map((voter) => voter.username));
|
||||
newVoters.forEach((voter) => {
|
||||
if (!votersSet.has(voter.username)) {
|
||||
votersSet.add(voter.username);
|
||||
voters.push(voter);
|
||||
}
|
||||
});
|
||||
// remove users who changed their vote
|
||||
if (this.poll.type === REGULAR) {
|
||||
Object.keys(this.preloadedVoters).forEach((otherOptionId) => {
|
||||
if (optionId !== otherOptionId) {
|
||||
this.preloadedVoters[otherOptionId] = this.preloadedVoters[
|
||||
otherOptionId
|
||||
].filter((voter) => !votersSet.has(voter.username));
|
||||
}
|
||||
});
|
||||
}
|
||||
this.preloadedVoters[optionId] = [
|
||||
...new Set([...this.preloadedVoters[optionId], ...newVoters]),
|
||||
];
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error) {
|
||||
popupAjaxError(error);
|
||||
} else {
|
||||
this.dialog.alert(I18n.t("poll.error_while_fetching_voters"));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
options.find((option) => option.id === optionId).loading = false;
|
||||
this.options = [...options];
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
dropDownClick(dropDownAction) {
|
||||
this[dropDownAction]();
|
||||
}
|
||||
|
||||
@action
|
||||
removeVote() {
|
||||
return ajax("/polls/vote", {
|
||||
type: "DELETE",
|
||||
data: {
|
||||
post_id: this.post.id,
|
||||
poll_name: this.poll.name,
|
||||
},
|
||||
})
|
||||
.then(({ poll }) => {
|
||||
this.options = [...poll.options];
|
||||
this.poll.setProperties(poll);
|
||||
this.vote = [];
|
||||
this.voters = poll.voters;
|
||||
this.hasSavedVote = false;
|
||||
this.appEvents.trigger("poll:voted", poll, this.post, this.vote);
|
||||
})
|
||||
.catch((error) => popupAjaxError(error));
|
||||
}
|
||||
|
||||
@action
|
||||
toggleStatus() {
|
||||
if (this.isAutomaticallyClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialog.yesNoConfirm({
|
||||
message: I18n.t(this.closed ? "poll.open.confirm" : "poll.close.confirm"),
|
||||
didConfirm: () => {
|
||||
const status = this.closed ? "open" : "closed";
|
||||
ajax("/polls/toggle_status", {
|
||||
type: "PUT",
|
||||
data: {
|
||||
post_id: this.post.id,
|
||||
poll_name: this.poll.name,
|
||||
status,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.poll.status = status;
|
||||
this.status = status;
|
||||
if (
|
||||
this.poll.results === "on_close" ||
|
||||
this.poll.results === "always"
|
||||
) {
|
||||
this.showResults = this.status === "closed";
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error) {
|
||||
popupAjaxError(error);
|
||||
} else {
|
||||
this.dialog.alert(I18n.t("poll.error_while_toggling_status"));
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
showBreakdown() {
|
||||
this.modal.show(PollBreakdownModal, {
|
||||
model: this.args.attrs,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
exportResults() {
|
||||
const queryID = this.siteSettings.poll_export_data_explorer_query_id;
|
||||
|
||||
// This uses the Data Explorer plugin export as CSV route
|
||||
// There is detection to check if the plugin is enabled before showing the button
|
||||
ajax(`/admin/plugins/explorer/queries/${queryID}/run.csv`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
// needed for data-explorer route compatibility
|
||||
params: JSON.stringify({
|
||||
poll_name: this.poll.name,
|
||||
post_id: this.post.id.toString(), // needed for data-explorer route compatibility
|
||||
}),
|
||||
explain: false,
|
||||
limit: 1000000,
|
||||
download: 1,
|
||||
},
|
||||
})
|
||||
.then((csvContent) => {
|
||||
const downloadLink = document.createElement("a");
|
||||
const blob = new Blob([csvContent], {
|
||||
type: "text/csv;charset=utf-8;",
|
||||
});
|
||||
downloadLink.href = URL.createObjectURL(blob);
|
||||
downloadLink.setAttribute(
|
||||
"download",
|
||||
`poll-export-${this.poll.name}-${this.post.id}.csv`
|
||||
);
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error) {
|
||||
popupAjaxError(error);
|
||||
} else {
|
||||
this.dialog.alert(I18n.t("poll.error_while_exporting_results"));
|
||||
}
|
||||
});
|
||||
}
|
||||
<template>
|
||||
<div
|
||||
{{didUpdate this.updatedVoters @preloadedVoters}}
|
||||
class="poll-container"
|
||||
>
|
||||
{{this.titleHTML}}
|
||||
{{#if this.notInVotingGroup}}
|
||||
<div class="alert alert-danger">{{this.pollGroups}}</div>
|
||||
{{/if}}
|
||||
{{#if this.showResults}}
|
||||
<div class={{this.resultsWidgetTypeClass}}>
|
||||
{{#if this.isNumber}}
|
||||
<span>{{this.averageRating}}</span>
|
||||
{{else}}
|
||||
{{#if this.resultsPie}}
|
||||
<PollResultsPie @id={{this.id}} @options={{this.options}} />
|
||||
{{else}}
|
||||
<PollResultsStandard
|
||||
@options={{this.options}}
|
||||
@pollName={{this.poll.name}}
|
||||
@pollType={{this.poll.type}}
|
||||
@isPublic={{this.poll.public}}
|
||||
@postId={{this.post.id}}
|
||||
@vote={{this.vote}}
|
||||
@voters={{this.preloadedVoters}}
|
||||
@votersCount={{this.poll.voters}}
|
||||
@fetchVoters={{this.fetchVoters}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
<PollOptions
|
||||
@isCheckbox={{this.isCheckbox}}
|
||||
@options={{this.options}}
|
||||
@votes={{this.vote}}
|
||||
@sendOptionSelect={{this.toggleOption}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<PollInfo
|
||||
@options={{this.options}}
|
||||
@min={{this.min}}
|
||||
@max={{this.max}}
|
||||
@isMultiple={{this.isMultiple}}
|
||||
@close={{this.close}}
|
||||
@closed={{this.closed}}
|
||||
@results={{this.poll.results}}
|
||||
@showResults={{this.showResults}}
|
||||
@postUserId={{this.poll.post.user_id}}
|
||||
@isPublic={{this.poll.public}}
|
||||
@hasVoted={{this.hasVoted}}
|
||||
@voters={{this.voters}}
|
||||
/>
|
||||
<div class="poll-buttons">
|
||||
{{#if this.showCastVotesButton}}
|
||||
<button
|
||||
class={{this.castVotesButtonClass}}
|
||||
title="poll.cast-votes.title"
|
||||
disabled={{this.castVotesDisabled}}
|
||||
{{on "click" this.castVotes}}
|
||||
>
|
||||
{{icon this.castVotesButtonIcon}}
|
||||
<span class="d-button-label">{{i18n "poll.cast-votes.label"}}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showHideResultsButton}}
|
||||
<button
|
||||
class="btn btn-default toggle-results"
|
||||
title="poll.hide-results.title"
|
||||
{{on "click" this.toggleResults}}
|
||||
>
|
||||
{{icon "chevron-left"}}
|
||||
<span class="d-button-label">{{i18n "poll.hide-results.label"}}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showShowResultsButton}}
|
||||
<button
|
||||
class="btn btn-default toggle-results"
|
||||
title="poll.show-results.title"
|
||||
{{on "click" this.toggleResults}}
|
||||
>
|
||||
{{icon "chart-bar"}}
|
||||
<span class="d-button-label">{{i18n "poll.show-results.label"}}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showRemoveVoteButton}}
|
||||
<button
|
||||
class="btn btn-default remove-vote"
|
||||
title="poll.remove-vote.title"
|
||||
{{on "click" this.removeVote}}
|
||||
>
|
||||
{{icon "undo"}}
|
||||
<span class="d-button-label">{{i18n "poll.remove-vote.label"}}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
<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}}
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,354 +1,432 @@
|
|||
div.poll {
|
||||
margin: 1em 0;
|
||||
border: 1px solid var(--primary-low);
|
||||
display: grid;
|
||||
grid-template-areas: "poll" "info" "buttons";
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
grid-template-columns: 1fr 10em;
|
||||
grid-template-areas: "poll info" "buttons buttons";
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
li[data-poll-option-id] {
|
||||
color: var(--primary);
|
||||
padding: 0.5em 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
img {
|
||||
// Hacky way to stop images without width/height
|
||||
// from causing abrupt unintended scrolling
|
||||
&:not([width]):not(.emoji),
|
||||
&:not([height]):not(.emoji) {
|
||||
width: 200px !important;
|
||||
height: 200px !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-info {
|
||||
box-sizing: border-box;
|
||||
grid-area: info;
|
||||
display: flex;
|
||||
line-height: var(--line-height-medium);
|
||||
color: var(--primary-medium);
|
||||
|
||||
@include breakpoint("mobile-extra-large") {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
flex-direction: row-reverse;
|
||||
&_counts,
|
||||
&_instructions {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
div.poll-outer {
|
||||
div.poll {
|
||||
margin: 1em 0;
|
||||
border: 1px solid var(--primary-low);
|
||||
display: grid;
|
||||
grid-template-areas: "poll" "info" "buttons";
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
gap: 0 1em;
|
||||
padding: 1em;
|
||||
border-left: 1px solid var(--primary-low);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr 10em;
|
||||
grid-template-areas: "poll info" "buttons buttons";
|
||||
}
|
||||
|
||||
&_counts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
gap: 0.25em 0;
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
justify-content: center;
|
||||
li[data-poll-option-id] {
|
||||
color: var(--primary);
|
||||
padding: 0.5em 0;
|
||||
word-break: break-word;
|
||||
button {
|
||||
background-color: var(--secondary);
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
// Hacky way to stop images without width/height
|
||||
// from causing abrupt unintended scrolling
|
||||
&:not([width]):not(.emoji),
|
||||
&:not([height]):not(.emoji) {
|
||||
width: 200px !important;
|
||||
height: 200px !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-info {
|
||||
box-sizing: border-box;
|
||||
grid-area: info;
|
||||
display: flex;
|
||||
line-height: var(--line-height-medium);
|
||||
color: var(--primary-medium);
|
||||
|
||||
@include breakpoint("mobile-extra-large") {
|
||||
flex: 1 1 auto;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
flex-direction: row-reverse;
|
||||
&_counts,
|
||||
&_instructions {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&-count {
|
||||
gap: 0.25em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
gap: 0 1em;
|
||||
padding: 1em;
|
||||
border-left: 1px solid var(--primary-low);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-label,
|
||||
.info-number {
|
||||
display: inline;
|
||||
margin-right: 0.25em;
|
||||
text-align: center;
|
||||
&_counts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 0.25em 0;
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large") {
|
||||
&:not(:last-child) {
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
&-count {
|
||||
gap: 0.25em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
|
||||
.info-label,
|
||||
.info-number {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
margin-right: 0.25em;
|
||||
text-align: center;
|
||||
}
|
||||
+ .poll-info_counts-count {
|
||||
|
||||
@include breakpoint("mobile-extra-large") {
|
||||
&:not(:last-child) {
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
gap: 0 0.33em;
|
||||
margin: 0.5em;
|
||||
.info-number,
|
||||
.info-label {
|
||||
font-size: var(--font-up-1);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.info-label,
|
||||
.info-number {
|
||||
margin: 0;
|
||||
}
|
||||
+ .poll-info_counts-count {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
line-height: var(--line-height-medium);
|
||||
gap: 0 0.33em;
|
||||
margin: 0.5em;
|
||||
.info-number,
|
||||
.info-label {
|
||||
font-size: var(--font-up-1);
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
line-height: var(--line-height-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
+ .poll-info_instructions:not(:empty) {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_instructions {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-self: start;
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
padding-top: 1.25em;
|
||||
&:not(:empty) {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large") {
|
||||
padding-right: 1em;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
&:not(:empty) {
|
||||
border-right: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
&:not(:first-child:last-child) {
|
||||
// only applied when there are multiple items
|
||||
.d-icon {
|
||||
width: 15%;
|
||||
}
|
||||
span {
|
||||
width: 85%;
|
||||
}
|
||||
}
|
||||
&:first-child:last-child {
|
||||
// when there's a single item, it looks better centered
|
||||
display: inline;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
font-size: var(--font-down-1);
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.info-text {
|
||||
margin: 0.25em 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
+ .poll-info_instructions:not(:empty) {
|
||||
margin-top: 1.25em;
|
||||
.info-label {
|
||||
font-size: var(--font-up-2);
|
||||
}
|
||||
.info-number {
|
||||
font-size: var(--font-up-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_instructions {
|
||||
.poll-container {
|
||||
box-sizing: border-box;
|
||||
grid-area: poll;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
align-self: center;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
|
||||
.poll-results-number-rating {
|
||||
font-size: var(--font-up-5);
|
||||
}
|
||||
}
|
||||
|
||||
.poll-title {
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
margin-bottom: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.poll-buttons {
|
||||
box-sizing: border-box;
|
||||
grid-area: buttons;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-self: start;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
align-self: start;
|
||||
.d-button-label {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
@include breakpoint("tablet") {
|
||||
flex: 1 1 0;
|
||||
&:first-child:last-child {
|
||||
// if there's only one button,
|
||||
// don't expand the width
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
&.toggle-results:first-child {
|
||||
// don't expand the back button
|
||||
flex: 0 1 auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-large") {
|
||||
&:first-child:last-child,
|
||||
&.cast-votes {
|
||||
// ok to expand button width on smaller screens
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
padding-top: 1.25em;
|
||||
&:not(:empty) {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large") {
|
||||
padding-right: 1em;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
&:not(:empty) {
|
||||
border-right: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
.poll-voters:not(:empty) {
|
||||
min-height: 30px;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
&:not(:first-child:last-child) {
|
||||
// only applied when there are multiple items
|
||||
.d-icon {
|
||||
width: 15%;
|
||||
}
|
||||
span {
|
||||
width: 85%;
|
||||
}
|
||||
}
|
||||
&:first-child:last-child {
|
||||
// when there's a single item, it looks better centered
|
||||
display: inline;
|
||||
text-align: center;
|
||||
}
|
||||
.poll-voters-toggle-expand {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.spinner {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
// .poll-buttons-dropdown {
|
||||
// align-self: stretch;
|
||||
// position: relative;
|
||||
|
||||
// .label {
|
||||
// display: none;
|
||||
// }
|
||||
// .widget-dropdown {
|
||||
// &.closed {
|
||||
// :not(:first-child) {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// &.opened {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// position: relative;
|
||||
|
||||
// overflow-y: visible;
|
||||
// .widget-dropdown-body {
|
||||
// display: block;
|
||||
// position: absolute;
|
||||
// z-index: 300;
|
||||
// overflow-y: visible;
|
||||
// transform: translate(-44px, 38px);
|
||||
// .widget-dropdown-item {
|
||||
// width: 100%;
|
||||
// padding: 0;
|
||||
// margin: 0;
|
||||
// float: left;
|
||||
// &:hover {
|
||||
// color: var(--tertiary);
|
||||
// background-color: var(--primary-low);
|
||||
// }
|
||||
// button {
|
||||
// width: 100%;
|
||||
// padding: 0.5em;
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// background-color: var(--secondary);
|
||||
// &:hover {
|
||||
// background-color: var(--primary-low);
|
||||
// }
|
||||
// border: none;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// &-header {
|
||||
// height: 100%;
|
||||
// .d-icon {
|
||||
// margin: 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
.poll-buttons-dropdown,
|
||||
.export-results,
|
||||
.toggle-status,
|
||||
.show-breakdown {
|
||||
// we want these controls to be separated
|
||||
// from voting controls
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.results {
|
||||
> li {
|
||||
cursor: default;
|
||||
padding: 0.25em 0;
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
font-size: var(--font-down-1);
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.info-text {
|
||||
margin: 0.25em 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large", min-width) {
|
||||
.info-label {
|
||||
font-size: var(--font-up-2);
|
||||
}
|
||||
.info-number {
|
||||
font-size: var(--font-up-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poll-container {
|
||||
box-sizing: border-box;
|
||||
grid-area: poll;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
|
||||
.poll-results-number-rating {
|
||||
font-size: var(--font-up-5);
|
||||
}
|
||||
}
|
||||
|
||||
.poll-title {
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
margin-bottom: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.poll-buttons {
|
||||
box-sizing: border-box;
|
||||
grid-area: buttons;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
align-self: start;
|
||||
.d-button-label {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
@include breakpoint("tablet") {
|
||||
flex: 1 1 0;
|
||||
&:first-child:last-child {
|
||||
// if there's only one button,
|
||||
// don't expand the width
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
&.toggle-results:first-child {
|
||||
// don't expand the back button
|
||||
flex: 0 1 auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-large") {
|
||||
&:first-child:last-child,
|
||||
&.cast-votes {
|
||||
// ok to expand button width on smaller screens
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-voters:not(:empty) {
|
||||
min-height: 30px;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-voters-toggle-expand {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.spinner {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-buttons-dropdown {
|
||||
align-self: stretch;
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
.widget-dropdown {
|
||||
height: 100%;
|
||||
|
||||
&-header {
|
||||
height: 100%;
|
||||
.d-icon {
|
||||
.option {
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poll-buttons-dropdown,
|
||||
.export-results,
|
||||
.toggle-status,
|
||||
.show-breakdown {
|
||||
// we want these controls to be separated
|
||||
// from voting controls
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.results {
|
||||
> li {
|
||||
cursor: default;
|
||||
padding: 0.25em 0;
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
.percentage {
|
||||
float: right;
|
||||
color: var(--primary-medium);
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
||||
.option {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
.bar-back {
|
||||
background: var(--primary-low);
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 0.75em;
|
||||
background: var(--primary-medium);
|
||||
}
|
||||
|
||||
.chosen .bar {
|
||||
background: var(--tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.percentage {
|
||||
float: right;
|
||||
color: var(--primary-medium);
|
||||
margin-left: 0.25em;
|
||||
.pie-chart-legends {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
|
||||
.legend {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
margin-left: 1em;
|
||||
font-size: var(--font-down-0);
|
||||
|
||||
.swatch {
|
||||
margin-right: 0.5em;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-back {
|
||||
background: var(--primary-low);
|
||||
.poll-grouped-pies-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 0.75em;
|
||||
background: var(--primary-medium);
|
||||
}
|
||||
|
||||
.chosen .bar {
|
||||
background: var(--tertiary);
|
||||
.poll-results-chart {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,38 +437,6 @@ div.poll {
|
|||
margin-right: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.pie-chart-legends {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
|
||||
.legend {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
margin-left: 1em;
|
||||
font-size: var(--font-down-0);
|
||||
|
||||
.swatch {
|
||||
margin-right: 0.5em;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poll-grouped-pies-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.poll-results-chart {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.d-editor-preview {
|
||||
|
|
|
@ -22,7 +22,6 @@ en:
|
|||
title: "Results will be shown once <strong>closed</strong>."
|
||||
staff:
|
||||
title: "Results are only shown to <strong>staff</strong> members."
|
||||
|
||||
multiple:
|
||||
help:
|
||||
at_least_min_options:
|
||||
|
@ -80,7 +79,7 @@ en:
|
|||
breakdown: "Breakdown"
|
||||
percentage: "Percentage"
|
||||
count: "Count"
|
||||
|
||||
|
||||
options:
|
||||
label: "Options"
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DiscoursePoll::Poll
|
||||
MULTIPLE = "multiple"
|
||||
REGULAR = "regular"
|
||||
|
||||
def self.vote(user, post_id, poll_name, options)
|
||||
poll_id = nil
|
||||
|
||||
|
@ -9,6 +12,7 @@ class DiscoursePoll::Poll
|
|||
poll_id = poll.id
|
||||
# remove options that aren't available in the poll
|
||||
available_options = poll.poll_options.map { |o| o.digest }.to_set
|
||||
|
||||
options.select! { |o| available_options.include?(o) }
|
||||
|
||||
if options.empty?
|
||||
|
@ -35,17 +39,21 @@ class DiscoursePoll::Poll
|
|||
PollVote.where(poll: poll, user: user).where.not(poll_option_id: new_option_ids).delete_all
|
||||
|
||||
# create missing votes
|
||||
(new_option_ids - old_option_ids).each do |option_id|
|
||||
creation_set = new_option_ids - old_option_ids
|
||||
|
||||
creation_set.each do |option_id|
|
||||
PollVote.create!(poll: poll, user: user, poll_option_id: option_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Ensure consistency here as we do not have a unique index to limit the
|
||||
# number of votes per the poll's configuration.
|
||||
is_multiple = serialized_poll[:type] == "multiple"
|
||||
is_multiple = serialized_poll[:type] == MULTIPLE
|
||||
offset = is_multiple ? (serialized_poll[:max] || serialized_poll[:options].length) : 1
|
||||
|
||||
DB.query(<<~SQL, poll_id: poll_id, user_id: user.id, offset: offset)
|
||||
params = { poll_id: poll_id, offset: offset, user_id: user.id }
|
||||
|
||||
DB.query(<<~SQL, params)
|
||||
DELETE FROM poll_votes
|
||||
USING (
|
||||
SELECT
|
||||
|
@ -61,6 +69,18 @@ class DiscoursePoll::Poll
|
|||
AND poll_votes.user_id = to_delete_poll_votes.user_id
|
||||
SQL
|
||||
|
||||
if serialized_poll[:type] == MULTIPLE
|
||||
serialized_poll[:options].each do |option|
|
||||
option.merge!(
|
||||
chosen:
|
||||
PollVote
|
||||
.joins(:poll_option)
|
||||
.where(poll_options: { digest: option[:id] }, user_id: user.id, poll_id: poll_id)
|
||||
.exists?,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
[serialized_poll, options]
|
||||
end
|
||||
|
||||
|
@ -159,7 +179,9 @@ class DiscoursePoll::Poll
|
|||
|
||||
result = { option_digest => user_hashes }
|
||||
else
|
||||
votes = DB.query <<~SQL
|
||||
params = { poll_id: poll.id, offset: offset, offset_plus_limit: offset + limit }
|
||||
|
||||
votes = DB.query(<<~SQL, params)
|
||||
SELECT digest, user_id
|
||||
FROM (
|
||||
SELECT digest
|
||||
|
@ -167,10 +189,10 @@ class DiscoursePoll::Poll
|
|||
, ROW_NUMBER() OVER (PARTITION BY poll_option_id ORDER BY pv.created_at) AS row
|
||||
FROM poll_votes pv
|
||||
JOIN poll_options po ON pv.poll_option_id = po.id
|
||||
WHERE pv.poll_id = #{poll.id}
|
||||
AND po.poll_id = #{poll.id}
|
||||
WHERE pv.poll_id = :poll_id
|
||||
AND po.poll_id = :poll_id
|
||||
) v
|
||||
WHERE row BETWEEN #{offset} AND #{offset + limit}
|
||||
WHERE row BETWEEN :offset AND :offset_plus_limit
|
||||
SQL
|
||||
|
||||
user_ids = votes.map(&:user_id).uniq
|
||||
|
@ -292,7 +314,7 @@ class DiscoursePoll::Poll
|
|||
post_id: post_id,
|
||||
name: poll["name"].presence || "poll",
|
||||
close_at: close_at,
|
||||
type: poll["type"].presence || "regular",
|
||||
type: poll["type"].presence || REGULAR,
|
||||
status: poll["status"].presence || "open",
|
||||
visibility: poll["public"] == "true" ? "everyone" : "secret",
|
||||
title: poll["title"],
|
||||
|
|
|
@ -65,14 +65,15 @@ acceptance("Poll breakdown", function (needs) {
|
|||
|
||||
test("Displaying the poll breakdown modal", async function (assert) {
|
||||
await visit("/t/-/topic_with_pie_chart_poll");
|
||||
|
||||
await click(".widget-dropdown-header");
|
||||
|
||||
assert.ok(
|
||||
exists(".item-showBreakdown"),
|
||||
exists("button.show-breakdown"),
|
||||
"shows the breakdown button when poll_groupable_user_fields is non-empty"
|
||||
);
|
||||
|
||||
await click(".item-showBreakdown");
|
||||
await click("button.show-breakdown");
|
||||
|
||||
assert.ok(exists(".poll-breakdown-total-votes"), "displays the vote count");
|
||||
|
||||
|
@ -91,7 +92,8 @@ acceptance("Poll breakdown", function (needs) {
|
|||
test("Changing the display mode from percentage to count", async function (assert) {
|
||||
await visit("/t/-/topic_with_pie_chart_poll");
|
||||
await click(".widget-dropdown-header");
|
||||
await click(".item-showBreakdown");
|
||||
|
||||
await click("button.show-breakdown");
|
||||
|
||||
assert.strictEqual(
|
||||
query(".poll-breakdown-option-count").textContent.trim(),
|
||||
|
|
|
@ -20,7 +20,7 @@ acceptance("Rendering polls with pie charts", function (needs) {
|
|||
.dom(".poll .poll-info_counts-count:last-child .info-number")
|
||||
.hasText("5", "it should display the right number of votes");
|
||||
|
||||
assert.dom(".poll").hasClass("pie", "pie class is present on poll div");
|
||||
assert.dom(".poll-outer").hasClass("pie", "pie class is present on poll div");
|
||||
|
||||
assert
|
||||
.dom(".poll .poll-results-chart")
|
||||
|
|
|
@ -552,7 +552,26 @@ acceptance("Poll results", function (needs) {
|
|||
}
|
||||
});
|
||||
|
||||
server.delete("/polls/vote", () => helper.response({ success: "OK" }));
|
||||
server.delete("/polls/vote", () =>
|
||||
helper.response({
|
||||
success: "OK",
|
||||
poll: {
|
||||
options: [
|
||||
{
|
||||
id: "db753fe0bc4e72869ac1ad8765341764",
|
||||
html: 'Option <span class="hashtag">#1</span>',
|
||||
votes: 0,
|
||||
},
|
||||
{
|
||||
id: "d8c22ff912e03740d9bc19e133e581e0",
|
||||
html: 'Option <span class="hashtag">#2</span>',
|
||||
votes: 0,
|
||||
},
|
||||
],
|
||||
voters: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("can load more voters", async function (assert) {
|
||||
|
@ -620,12 +639,13 @@ acceptance("Poll results", function (needs) {
|
|||
count(".poll-container .results li:nth-child(1) .poll-voters li"),
|
||||
1
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
count(".poll-container .results li:nth-child(2) .poll-voters li"),
|
||||
1
|
||||
);
|
||||
|
||||
await click(".poll-voters-toggle-expand a");
|
||||
await click(".poll-voters-toggle-expand");
|
||||
await visit("/t/load-more-poll-voters/134");
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -640,6 +660,7 @@ acceptance("Poll results", function (needs) {
|
|||
|
||||
test("can unvote", async function (assert) {
|
||||
await visit("/t/load-more-poll-voters/134");
|
||||
|
||||
await click(".toggle-results");
|
||||
|
||||
assert.strictEqual(count(".poll-container .d-icon-circle"), 1);
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { click, render } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { count, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import I18n from "I18n";
|
||||
|
||||
module("Poll | Component | poll-buttons-dropdown", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("Renders a clickable dropdown menu with a close option", async function (assert) {
|
||||
this.setProperties({
|
||||
closed: false,
|
||||
voters: [],
|
||||
isStaff: true,
|
||||
isMe: false,
|
||||
topicArchived: false,
|
||||
groupableUserFields: [],
|
||||
isAutomaticallyClosed: false,
|
||||
dropDownClick: () => {},
|
||||
});
|
||||
|
||||
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}}
|
||||
/>`);
|
||||
|
||||
await click(".widget-dropdown-header");
|
||||
|
||||
assert.strictEqual(count("li.dropdown-menu__item"), 1);
|
||||
|
||||
assert.strictEqual(
|
||||
query("li.dropdown-menu__item span").textContent.trim(),
|
||||
I18n.t("poll.close.label"),
|
||||
"displays the poll Close action"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import I18n from "I18n";
|
||||
|
||||
const OPTIONS = [
|
||||
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 2, rank: 0 },
|
||||
{ id: "70e743697dac09483d7b824eaadb91e1", html: "That", votes: 3, rank: 0 },
|
||||
{ id: "6c986ebcde3d5822a6e91a695c388094", html: "Other", votes: 5, rank: 0 },
|
||||
];
|
||||
|
||||
module("Poll | Component | poll-info", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("multiple poll", async function (assert) {
|
||||
this.setProperties({
|
||||
isMultiple: true,
|
||||
min: 1,
|
||||
max: 2,
|
||||
options: OPTIONS,
|
||||
close: null,
|
||||
closed: false,
|
||||
results: [],
|
||||
showResults: false,
|
||||
postUserId: 59,
|
||||
isPublic: true,
|
||||
hasVoted: true,
|
||||
voters: [],
|
||||
});
|
||||
|
||||
await render(hbs`<PollInfo
|
||||
@options={{this.options}}
|
||||
@min={{this.min}}
|
||||
@max={{this.max}}
|
||||
@isMultiple={{this.isMultiple}}
|
||||
@close={{this.close}}
|
||||
@closed={{this.closed}}
|
||||
@results={{this.results}}
|
||||
@showResults={{this.showResults}}
|
||||
@postUserId={{this.postUserId}}
|
||||
@isPublic={{this.isPublic}}
|
||||
@hasVoted={{this.hasVoted}}
|
||||
@voters={{this.voters}}
|
||||
/>`);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".poll-info_instructions li.multiple-help-text").textContent.trim(),
|
||||
I18n.t("poll.multiple.help.up_to_max_options", {
|
||||
count: this.max,
|
||||
}).replace(/<\/?[^>]+(>|$)/g, ""),
|
||||
"displays the multiple help text"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".poll-info_instructions li.is-public").textContent.trim(),
|
||||
I18n.t("poll.public.title").replace(/<\/?[^>]+(>|$)/g, ""),
|
||||
"displays the public label"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { count } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
const OPTIONS = [
|
||||
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 0, rank: 0 },
|
||||
{ id: "70e743697dac09483d7b824eaadb91e1", html: "That", votes: 0, rank: 0 },
|
||||
{ id: "6c986ebcde3d5822a6e91a695c388094", html: "Other", votes: 0, rank: 0 },
|
||||
];
|
||||
|
||||
module("Poll | Component | poll-options", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("single, not selected", async function (assert) {
|
||||
this.setProperties({
|
||||
isCheckbox: false,
|
||||
options: OPTIONS,
|
||||
votes: [],
|
||||
});
|
||||
|
||||
await render(hbs`<PollOptions
|
||||
@isCheckbox={{this.isCheckbox}}
|
||||
@options={{this.options}}
|
||||
@votes={{this.votes}}
|
||||
@sendRadioClick={{this.toggleOption}}
|
||||
/>`);
|
||||
|
||||
assert.strictEqual(count("li .d-icon-far-circle:nth-of-type(1)"), 3);
|
||||
});
|
||||
|
||||
test("single, selected", async function (assert) {
|
||||
this.setProperties({
|
||||
isCheckbox: false,
|
||||
options: OPTIONS,
|
||||
votes: ["6c986ebcde3d5822a6e91a695c388094"],
|
||||
});
|
||||
|
||||
await render(hbs`<PollOptions
|
||||
@isCheckbox={{this.isCheckbox}}
|
||||
@options={{this.options}}
|
||||
@votes={{this.votes}}
|
||||
@sendRadioClick={{this.toggleOption}}
|
||||
/>`);
|
||||
|
||||
assert.strictEqual(count("li .d-icon-circle:nth-of-type(1)"), 1);
|
||||
});
|
||||
|
||||
test("multi, not selected", async function (assert) {
|
||||
this.setProperties({
|
||||
isCheckbox: true,
|
||||
options: OPTIONS,
|
||||
votes: [],
|
||||
});
|
||||
|
||||
await render(hbs`<PollOptions
|
||||
@isCheckbox={{this.isCheckbox}}
|
||||
@options={{this.options}}
|
||||
@votes={{this.votes}}
|
||||
@sendRadioClick={{this.toggleOption}}
|
||||
/>`);
|
||||
|
||||
assert.strictEqual(count("li .d-icon-far-square:nth-of-type(1)"), 3);
|
||||
});
|
||||
|
||||
test("multi, selected", async function (assert) {
|
||||
this.setProperties({
|
||||
isCheckbox: true,
|
||||
options: OPTIONS,
|
||||
votes: ["6c986ebcde3d5822a6e91a695c388094"],
|
||||
});
|
||||
|
||||
await render(hbs`<PollOptions
|
||||
@isCheckbox={{this.isCheckbox}}
|
||||
@options={{this.options}}
|
||||
@votes={{this.votes}}
|
||||
@sendRadioClick={{this.toggleOption}}
|
||||
/>`);
|
||||
|
||||
assert.strictEqual(count("li .d-icon-far-check-square:nth-of-type(1)"), 1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { count } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
const OPTIONS = [
|
||||
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 3, rank: 0 },
|
||||
{ id: "70e743697dac09483d7b824eaadb91e1", html: "That", votes: 1, rank: 0 },
|
||||
{ id: "6c986ebcde3d5822a6e91a695c388094", html: "Other", votes: 2, rank: 0 },
|
||||
];
|
||||
|
||||
const ID = "23";
|
||||
|
||||
module("Poll | Component | poll-results-pie", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("Renders the pie chart Component correctly", async function (assert) {
|
||||
this.setProperties({
|
||||
id: ID,
|
||||
options: OPTIONS,
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<PollResultsPie @id={{this.id}} @options={{this.options}} />`
|
||||
);
|
||||
|
||||
assert.strictEqual(count("li.legend"), 3);
|
||||
|
||||
assert.strictEqual(count("canvas.poll-results-canvas"), 1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,132 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { queryAll } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
const TWO_OPTIONS = [
|
||||
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 5, rank: 0 },
|
||||
{ id: "70e743697dac09483d7b824eaadb91e1", html: "That", votes: 4, rank: 0 },
|
||||
];
|
||||
|
||||
const TWO_OPTIONS_REVERSED = [
|
||||
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 4, rank: 0 },
|
||||
{ id: "70e743697dac09483d7b824eaadb91e1", html: "That", votes: 5, rank: 0 },
|
||||
];
|
||||
|
||||
const FIVE_OPTIONS = [
|
||||
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "a", votes: 5, rank: 0 },
|
||||
{ id: "70e743697dac09483d7b824eaadb91e1", html: "b", votes: 2, rank: 0 },
|
||||
{ id: "6c986ebcde3d5822a6e91a695c388094", html: "c", votes: 4, rank: 0 },
|
||||
{ id: "3e4a8f7e5f9d02f2bd7aebdb345c6ec2", html: "b", votes: 1, rank: 0 },
|
||||
{ id: "a5e3e8c77ac2b1f43dd98ee6ff9d34e3", html: "a", votes: 1, rank: 0 },
|
||||
];
|
||||
|
||||
const PRELOADEDVOTERS = {
|
||||
db753fe0bc4e72869ac1ad8765341764: [
|
||||
{
|
||||
id: 1,
|
||||
username: "bianca",
|
||||
name: null,
|
||||
avatar_template: "/letter_avatar_proxy/v4/letter/b/3be4f8/{size}.png",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
module("Poll | Component | poll-results-standard", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("Renders the standard results Component correctly", async function (assert) {
|
||||
this.setProperties({
|
||||
options: TWO_OPTIONS,
|
||||
pollName: "Two Choice Poll",
|
||||
pollType: "single",
|
||||
postId: 123,
|
||||
vote: ["1ddc47be0d2315b9711ee8526ca9d83f"],
|
||||
voters: PRELOADEDVOTERS,
|
||||
votersCount: 9,
|
||||
fetchVoters: () => {},
|
||||
});
|
||||
|
||||
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}}
|
||||
/>`);
|
||||
|
||||
assert.strictEqual(queryAll(".option .percentage")[0].innerText, "56%");
|
||||
assert.strictEqual(queryAll(".option .percentage")[1].innerText, "44%");
|
||||
});
|
||||
|
||||
test("options in ascending order", async function (assert) {
|
||||
this.setProperties({
|
||||
options: TWO_OPTIONS_REVERSED,
|
||||
pollName: "Two Choice Poll",
|
||||
pollType: "single",
|
||||
postId: 123,
|
||||
vote: ["1ddc47be0d2315b9711ee8526ca9d83f"],
|
||||
voters: PRELOADEDVOTERS,
|
||||
votersCount: 9,
|
||||
fetchVoters: () => {},
|
||||
});
|
||||
|
||||
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}}
|
||||
/>`);
|
||||
|
||||
assert.strictEqual(queryAll(".option .percentage")[0].innerText, "56%");
|
||||
assert.strictEqual(queryAll(".option .percentage")[1].innerText, "44%");
|
||||
});
|
||||
|
||||
test("options in ascending order", async function (assert) {
|
||||
this.setProperties({
|
||||
options: FIVE_OPTIONS,
|
||||
pollName: "Five Multi Option Poll",
|
||||
pollType: "multiple",
|
||||
postId: 123,
|
||||
vote: ["1ddc47be0d2315b9711ee8526ca9d83f"],
|
||||
voters: PRELOADEDVOTERS,
|
||||
votersCount: 12,
|
||||
fetchVoters: () => {},
|
||||
});
|
||||
|
||||
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}}
|
||||
/>`);
|
||||
|
||||
let percentages = queryAll(".option .percentage");
|
||||
assert.strictEqual(percentages[0].innerText, "41%");
|
||||
assert.strictEqual(percentages[1].innerText, "33%");
|
||||
assert.strictEqual(percentages[2].innerText, "16%");
|
||||
assert.strictEqual(percentages[3].innerText, "8%");
|
||||
|
||||
assert.strictEqual(
|
||||
queryAll(".option")[3].querySelectorAll("span")[1].innerText,
|
||||
"a"
|
||||
);
|
||||
assert.strictEqual(percentages[4].innerText, "8%");
|
||||
assert.strictEqual(
|
||||
queryAll(".option")[4].querySelectorAll("span")[1].innerText,
|
||||
"b"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -14,7 +14,7 @@ import I18n from "discourse-i18n";
|
|||
|
||||
let requests = 0;
|
||||
|
||||
module("Integration | Component | Widget | discourse-poll", function (hooks) {
|
||||
module("Poll | Component | poll", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
|
@ -47,14 +47,14 @@ module("Integration | Component | Widget | discourse-poll", function (hooks) {
|
|||
});
|
||||
|
||||
test("can vote", async function (assert) {
|
||||
this.set(
|
||||
"args",
|
||||
EmberObject.create({
|
||||
this.setProperties({
|
||||
attributes: EmberObject.create({
|
||||
post: EmberObject.create({
|
||||
id: 42,
|
||||
topic: {
|
||||
archived: false,
|
||||
},
|
||||
user_id: 29,
|
||||
}),
|
||||
poll: EmberObject.create({
|
||||
name: "poll",
|
||||
|
@ -70,16 +70,23 @@ module("Integration | Component | Widget | discourse-poll", function (hooks) {
|
|||
}),
|
||||
vote: [],
|
||||
groupableUserFields: [],
|
||||
})
|
||||
);
|
||||
}),
|
||||
preloadedVoters: [],
|
||||
options: [
|
||||
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes", votes: 0 },
|
||||
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no", votes: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="discourse-poll" @args={{this.args}} />`
|
||||
hbs`<Poll @attrs={{this.attributes}} @preloadedVoters={{this.preloadedVoters}} @options={{this.options}} />`
|
||||
);
|
||||
|
||||
requests = 0;
|
||||
|
||||
await click("li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29']");
|
||||
await click(
|
||||
"li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button"
|
||||
);
|
||||
assert.strictEqual(requests, 1);
|
||||
assert.strictEqual(count(".chosen"), 1);
|
||||
assert.deepEqual(
|
||||
|
@ -95,14 +102,14 @@ module("Integration | Component | Widget | discourse-poll", function (hooks) {
|
|||
});
|
||||
|
||||
test("cannot vote if not member of the right group", async function (assert) {
|
||||
this.set(
|
||||
"args",
|
||||
EmberObject.create({
|
||||
this.setProperties({
|
||||
attributes: EmberObject.create({
|
||||
post: EmberObject.create({
|
||||
id: 42,
|
||||
topic: {
|
||||
archived: false,
|
||||
},
|
||||
user_id: 29,
|
||||
}),
|
||||
poll: EmberObject.create({
|
||||
name: "poll",
|
||||
|
@ -119,16 +126,23 @@ module("Integration | Component | Widget | discourse-poll", function (hooks) {
|
|||
}),
|
||||
vote: [],
|
||||
groupableUserFields: [],
|
||||
})
|
||||
);
|
||||
}),
|
||||
preloadedVoters: [],
|
||||
options: [
|
||||
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes", votes: 0 },
|
||||
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no", votes: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="discourse-poll" @args={{this.args}} />`
|
||||
hbs`<Poll @attrs={{this.attributes}} @preloadedVoters={{this.preloadedVoters}} @options={{this.options}} />`
|
||||
);
|
||||
|
||||
requests = 0;
|
||||
|
||||
await click("li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29']");
|
||||
await click(
|
||||
"li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
query(".poll-container .alert").innerText,
|
||||
I18n.t("poll.results.groups.title", { groups: "foo" })
|
||||
|
@ -138,14 +152,14 @@ module("Integration | Component | Widget | discourse-poll", function (hooks) {
|
|||
});
|
||||
|
||||
test("voting on a multiple poll with no min attribute", async function (assert) {
|
||||
this.set(
|
||||
"args",
|
||||
EmberObject.create({
|
||||
this.setProperties({
|
||||
attributes: EmberObject.create({
|
||||
post: EmberObject.create({
|
||||
id: 42,
|
||||
topic: {
|
||||
archived: false,
|
||||
},
|
||||
user_id: 29,
|
||||
}),
|
||||
poll: EmberObject.create({
|
||||
name: "poll",
|
||||
|
@ -162,15 +176,23 @@ module("Integration | Component | Widget | discourse-poll", function (hooks) {
|
|||
}),
|
||||
vote: [],
|
||||
groupableUserFields: [],
|
||||
})
|
||||
);
|
||||
|
||||
}),
|
||||
preloadedVoters: [],
|
||||
options: [
|
||||
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes", votes: 0 },
|
||||
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no", votes: 0 },
|
||||
],
|
||||
});
|
||||
await render(
|
||||
hbs`<MountWidget @widget="discourse-poll" @args={{this.args}} />`
|
||||
hbs`<Poll @attrs={{this.attributes}} @preloadedVoters={{this.preloadedVoters}} @options={{this.options}} />`
|
||||
);
|
||||
|
||||
assert.ok(exists(".poll-buttons .cast-votes:disabled"));
|
||||
|
||||
await click(
|
||||
"li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button"
|
||||
);
|
||||
assert.ok(exists(".poll-buttons .cast-votes[disabled=true]"));
|
||||
|
||||
await click("li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29']");
|
||||
await click(".poll-buttons .cast-votes");
|
||||
assert.ok(exists(".chosen"));
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { count } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
module(
|
||||
"Integration | Component | Widget | discourse-poll-option",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const template = hbs`
|
||||
<MountWidget
|
||||
@widget="discourse-poll-option"
|
||||
@args={{hash
|
||||
option=this.option
|
||||
isMultiple=this.isMultiple
|
||||
vote=this.vote
|
||||
}}
|
||||
/>
|
||||
`;
|
||||
|
||||
test("single, not selected", async function (assert) {
|
||||
this.set("option", { id: "opt-id" });
|
||||
this.set("vote", []);
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.strictEqual(count("li .d-icon-far-circle:nth-of-type(1)"), 1);
|
||||
});
|
||||
|
||||
test("single, selected", async function (assert) {
|
||||
this.set("option", { id: "opt-id" });
|
||||
this.set("vote", ["opt-id"]);
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.strictEqual(count("li .d-icon-circle:nth-of-type(1)"), 1);
|
||||
});
|
||||
|
||||
test("multi, not selected", async function (assert) {
|
||||
this.setProperties({
|
||||
option: { id: "opt-id" },
|
||||
isMultiple: true,
|
||||
vote: [],
|
||||
});
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.strictEqual(count("li .d-icon-far-square:nth-of-type(1)"), 1);
|
||||
});
|
||||
|
||||
test("multi, selected", async function (assert) {
|
||||
this.setProperties({
|
||||
option: { id: "opt-id" },
|
||||
isMultiple: true,
|
||||
vote: ["opt-id"],
|
||||
});
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.strictEqual(
|
||||
count("li .d-icon-far-check-square:nth-of-type(1)"),
|
||||
1
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -1,86 +0,0 @@
|
|||
import EmberObject from "@ember/object";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { queryAll } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
module(
|
||||
"Integration | Component | Widget | discourse-poll-standard-results",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const template = hbs`
|
||||
<MountWidget
|
||||
@widget="discourse-poll-standard-results"
|
||||
@args={{hash poll=this.poll isMultiple=this.isMultiple}}
|
||||
/>
|
||||
`;
|
||||
|
||||
test("options in descending order", async function (assert) {
|
||||
this.set(
|
||||
"poll",
|
||||
EmberObject.create({
|
||||
options: [{ votes: 5 }, { votes: 4 }],
|
||||
voters: 9,
|
||||
})
|
||||
);
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.strictEqual(queryAll(".option .percentage")[0].innerText, "56%");
|
||||
assert.strictEqual(queryAll(".option .percentage")[1].innerText, "44%");
|
||||
});
|
||||
|
||||
test("options in ascending order", async function (assert) {
|
||||
this.set(
|
||||
"poll",
|
||||
EmberObject.create({
|
||||
options: [{ votes: 4 }, { votes: 5 }],
|
||||
voters: 9,
|
||||
})
|
||||
);
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.strictEqual(queryAll(".option .percentage")[0].innerText, "56%");
|
||||
assert.strictEqual(queryAll(".option .percentage")[1].innerText, "44%");
|
||||
});
|
||||
|
||||
test("multiple options in descending order", async function (assert) {
|
||||
this.set("isMultiple", true);
|
||||
this.set(
|
||||
"poll",
|
||||
EmberObject.create({
|
||||
type: "multiple",
|
||||
options: [
|
||||
{ votes: 5, html: "a" },
|
||||
{ votes: 2, html: "b" },
|
||||
{ votes: 4, html: "c" },
|
||||
{ votes: 1, html: "b" },
|
||||
{ votes: 1, html: "a" },
|
||||
],
|
||||
voters: 12,
|
||||
})
|
||||
);
|
||||
|
||||
await render(template);
|
||||
|
||||
let percentages = queryAll(".option .percentage");
|
||||
assert.strictEqual(percentages[0].innerText, "41%");
|
||||
assert.strictEqual(percentages[1].innerText, "33%");
|
||||
assert.strictEqual(percentages[2].innerText, "16%");
|
||||
assert.strictEqual(percentages[3].innerText, "8%");
|
||||
|
||||
assert.strictEqual(
|
||||
queryAll(".option")[3].querySelectorAll("span")[1].innerText,
|
||||
"a"
|
||||
);
|
||||
assert.strictEqual(percentages[4].innerText, "8%");
|
||||
assert.strictEqual(
|
||||
queryAll(".option")[4].querySelectorAll("span")[1].innerText,
|
||||
"b"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue