UX: Improve poll builder UI (#12549)

* Improve poll validation

* Redesign poll builder

* Group all advanced settings under a new section
This commit is contained in:
Bianca Nenciu 2021-04-12 19:48:01 +03:00 committed by GitHub
parent 7d1cef71ff
commit 2081b6e5c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 544 additions and 622 deletions

View File

@ -1,234 +1,152 @@
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import EmberObject from "@ember/object";
import EmberObject, { action } from "@ember/object";
import { gt, or } from "@ember/object/computed";
import { next } from "@ember/runloop";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import I18n from "I18n";
export const BAR_CHART_TYPE = "bar";
export const PIE_CHART_TYPE = "pie";
export default Controller.extend({
regularPollType: "regular",
numberPollType: "number",
multiplePollType: "multiple",
export const REGULAR_POLL_TYPE = "regular";
export const NUMBER_POLL_TYPE = "number";
export const MULTIPLE_POLL_TYPE = "multiple";
alwaysPollResult: "always",
votePollResult: "on_vote",
closedPollResult: "on_close",
staffPollResult: "staff_only",
pollChartTypes: [
{
name: I18n.t("poll.ui_builder.poll_chart_type.bar"),
value: BAR_CHART_TYPE,
},
{
name: I18n.t("poll.ui_builder.poll_chart_type.pie"),
value: PIE_CHART_TYPE,
},
],
const ALWAYS_POLL_RESULT = "always";
const VOTE_POLL_RESULT = "on_vote";
const CLOSED_POLL_RESULT = "on_close";
const STAFF_POLL_RESULT = "staff_only";
pollType: null,
pollResult: null,
pollTitle: null,
export default Controller.extend(ModalFunctionality, {
showAdvanced: false,
init() {
this._super(...arguments);
this._setupPoll();
pollType: REGULAR_POLL_TYPE,
pollTitle: "",
pollOptions: null,
pollMin: 1,
pollMax: 2,
pollStep: 1,
pollGroups: null,
pollAutoClose: null,
pollResult: ALWAYS_POLL_RESULT,
chartType: BAR_CHART_TYPE,
publicPoll: null,
onShow() {
this.setProperties({
showAdvanced: false,
pollType: REGULAR_POLL_TYPE,
pollTitle: null,
pollOptions: [EmberObject.create({ value: "" })],
pollMin: 1,
pollMax: 2,
pollStep: 1,
pollGroups: null,
pollAutoClose: null,
pollResult: ALWAYS_POLL_RESULT,
chartType: BAR_CHART_TYPE,
publicPoll: false,
});
},
@discourseComputed("regularPollType", "numberPollType", "multiplePollType")
pollTypes(regularPollType, numberPollType, multiplePollType) {
return [
{
name: I18n.t("poll.ui_builder.poll_type.regular"),
value: regularPollType,
},
{
name: I18n.t("poll.ui_builder.poll_type.number"),
value: numberPollType,
},
{
name: I18n.t("poll.ui_builder.poll_type.multiple"),
value: multiplePollType,
},
];
},
@discourseComputed("chartType", "pollType", "numberPollType")
isPie(chartType, pollType, numberPollType) {
return pollType !== numberPollType && chartType === PIE_CHART_TYPE;
},
@discourseComputed(
"alwaysPollResult",
"votePollResult",
"closedPollResult",
"staffPollResult"
)
pollResults(
alwaysPollResult,
votePollResult,
closedPollResult,
staffPollResult
) {
let options = [
@discourseComputed
pollResults() {
const options = [
{
name: I18n.t("poll.ui_builder.poll_result.always"),
value: alwaysPollResult,
value: ALWAYS_POLL_RESULT,
},
{
name: I18n.t("poll.ui_builder.poll_result.vote"),
value: votePollResult,
value: VOTE_POLL_RESULT,
},
{
name: I18n.t("poll.ui_builder.poll_result.closed"),
value: closedPollResult,
value: CLOSED_POLL_RESULT,
},
];
if (this.get("currentUser.staff")) {
options.push({
name: I18n.t("poll.ui_builder.poll_result.staff"),
value: staffPollResult,
value: STAFF_POLL_RESULT,
});
}
return options;
},
@discourseComputed("pollType")
isRegular(pollType) {
return pollType === REGULAR_POLL_TYPE;
},
@discourseComputed("pollType")
isNumber(pollType) {
return pollType === NUMBER_POLL_TYPE;
},
@discourseComputed("pollType")
isMultiple(pollType) {
return pollType === MULTIPLE_POLL_TYPE;
},
showNumber: or("showAdvanced", "isNumber"),
@discourseComputed("pollOptions.@each.value")
pollOptionsCount(pollOptions) {
return pollOptions.filter((option) => option.value.length > 0).length;
},
@discourseComputed("site.groups")
siteGroups(groups) {
return groups
.map((g) => {
// prevents group "everyone" to be listed
if (g.id !== 0) {
return { name: g.name };
}
})
.filter(Boolean);
// prevents group "everyone" to be listed
return groups.filter((g) => g.id !== 0);
},
@discourseComputed("pollType", "regularPollType")
isRegular(pollType, regularPollType) {
return pollType === regularPollType;
@discourseComputed("chartType", "pollType")
isPie(chartType, pollType) {
return pollType !== NUMBER_POLL_TYPE && chartType === PIE_CHART_TYPE;
},
@discourseComputed("pollType", "pollOptionsCount", "multiplePollType")
isMultiple(pollType, count, multiplePollType) {
return pollType === multiplePollType && count > 0;
},
@discourseComputed("pollType", "numberPollType")
isNumber(pollType, numberPollType) {
return pollType === numberPollType;
},
@discourseComputed("isRegular")
showMinMax(isRegular) {
return !isRegular;
},
@discourseComputed("pollOptions")
pollOptionsCount(pollOptions) {
if (pollOptions.length === 0) {
return 0;
}
let length = 0;
pollOptions.split("\n").forEach((option) => {
if (option.length !== 0) {
length += 1;
}
});
return length;
},
canRemoveOption: gt("pollOptions.length", 1),
@observes("pollType", "pollOptionsCount")
_setPollMax() {
const isMultiple = this.isMultiple;
const isNumber = this.isNumber;
if (!isMultiple && !isNumber) {
return;
}
_setPollMinMax() {
if (this.isMultiple) {
if (
this.pollMin >= this.pollMax ||
this.pollMin >= this.pollOptionsCount
) {
this.set("pollMin", this.pollOptionsCount > 0 ? 1 : 0);
}
if (isMultiple) {
this.set("pollMax", this.pollOptionsCount);
} else if (isNumber) {
if (
this.pollMin >= this.pollMax ||
this.pollMax > this.pollOptionsCount
) {
this.set("pollMax", Math.min(this.pollMin + 1, this.pollOptionsCount));
}
} else if (this.isNumber) {
this.set("pollMax", this.siteSettings.poll_maximum_options);
}
},
@discourseComputed("isRegular", "isMultiple", "isNumber", "pollOptionsCount")
pollMinOptions(isRegular, isMultiple, isNumber, count) {
if (isRegular) {
return;
}
if (isMultiple) {
return this._comboboxOptions(1, count + 1);
} else if (isNumber) {
return this._comboboxOptions(
1,
this.siteSettings.poll_maximum_options + 1
);
}
},
@discourseComputed(
"isRegular",
"isMultiple",
"isNumber",
"pollOptionsCount",
"pollMin",
"pollStep"
)
pollMaxOptions(isRegular, isMultiple, isNumber, count, pollMin, pollStep) {
if (isRegular) {
return;
}
const pollMinInt = parseInt(pollMin, 10) || 1;
if (isMultiple) {
return this._comboboxOptions(pollMinInt + 1, count + 1);
} else if (isNumber) {
let pollStepInt = parseInt(pollStep, 10);
if (pollStepInt < 1) {
pollStepInt = 1;
}
return this._comboboxOptions(
pollMinInt + 1,
pollMinInt + this.siteSettings.poll_maximum_options * pollStepInt
);
}
},
@discourseComputed("isNumber", "pollMax")
pollStepOptions(isNumber, pollMax) {
if (!isNumber) {
return;
}
return this._comboboxOptions(1, (parseInt(pollMax, 10) || 1) + 1);
},
@discourseComputed(
"isNumber",
"showMinMax",
"pollType",
"pollResult",
"publicPoll",
"pollTitle",
"pollOptions",
"pollOptions.@each.value",
"pollMin",
"pollMax",
"pollStep",
"pollGroups",
"autoClose",
"chartType",
"date",
"time"
"pollAutoClose",
"chartType"
)
pollOutput(
isNumber,
showMinMax,
pollType,
pollResult,
publicPoll,
@ -238,10 +156,8 @@ export default Controller.extend({
pollMax,
pollStep,
pollGroups,
autoClose,
chartType,
date,
time
pollAutoClose,
chartType
) {
let pollHeader = "[poll";
let output = "";
@ -265,32 +181,26 @@ export default Controller.extend({
if (pollResult) {
pollHeader += ` results=${pollResult}`;
}
if (pollMin && showMinMax) {
if (pollMin && pollType !== REGULAR_POLL_TYPE) {
pollHeader += ` min=${pollMin}`;
}
if (pollMax) {
if (pollMax && pollType !== REGULAR_POLL_TYPE) {
pollHeader += ` max=${pollMax}`;
}
if (isNumber) {
if (pollType === NUMBER_POLL_TYPE) {
pollHeader += ` step=${step}`;
}
if (publicPoll) {
pollHeader += ` public=true`;
}
if (chartType && pollType !== "number") {
if (chartType && pollType !== NUMBER_POLL_TYPE) {
pollHeader += ` chartType=${chartType}`;
}
if (pollGroups && pollGroups.length > 0) {
pollHeader += ` groups=${pollGroups}`;
}
if (autoClose) {
let closeDate = moment(
date + " " + time,
"YYYY-MM-DD HH:mm"
).toISOString();
if (closeDate) {
pollHeader += ` close=${closeDate}`;
}
if (pollAutoClose) {
pollHeader += ` close=${pollAutoClose.toISOString()}`;
}
pollHeader += "]";
@ -300,10 +210,10 @@ export default Controller.extend({
output += `# ${pollTitle.trim()}\n`;
}
if (pollOptions.length > 0 && !isNumber) {
pollOptions.split("\n").forEach((option) => {
if (option.length !== 0) {
output += `* ${option}\n`;
if (pollOptions.length > 0 && pollType !== NUMBER_POLL_TYPE) {
pollOptions.forEach((option) => {
if (option.value.length > 0) {
output += `* ${option.value.trim()}\n`;
}
});
}
@ -312,55 +222,11 @@ export default Controller.extend({
return output;
},
@discourseComputed(
"pollOptionsCount",
"isRegular",
"isMultiple",
"isNumber",
"pollMin",
"pollMax"
)
disableInsert(count, isRegular, isMultiple, isNumber, pollMin, pollMax) {
return (
(isRegular && count < 1) ||
(isMultiple && count < pollMin && pollMin >= pollMax) ||
(isNumber ? false : count < 1)
);
},
@discourseComputed("pollMin", "pollMax")
minMaxValueValidation(pollMin, pollMax) {
@discourseComputed("isNumber", "pollOptionsCount")
minNumOfOptionsValidation(isNumber, pollOptionsCount) {
let options = { ok: true };
if (pollMin >= pollMax) {
options = {
failed: true,
reason: I18n.t("poll.ui_builder.help.invalid_values"),
};
}
return EmberObject.create(options);
},
@discourseComputed("pollStep")
minStepValueValidation(pollStep) {
let options = { ok: true };
if (pollStep < 1) {
options = {
failed: true,
reason: I18n.t("poll.ui_builder.help.min_step_value"),
};
}
return EmberObject.create(options);
},
@discourseComputed("disableInsert")
minNumOfOptionsValidation(disableInsert) {
let options = { ok: true };
if (disableInsert) {
if (!isNumber && pollOptionsCount === 0) {
options = {
failed: true,
reason: I18n.t("poll.ui_builder.help.options_count"),
@ -370,6 +236,81 @@ export default Controller.extend({
return EmberObject.create(options);
},
@discourseComputed("pollOptions.@each.value")
showMinNumOfOptionsValidation(pollOptions) {
return pollOptions.length !== 1 || pollOptions[0].value !== "";
},
@discourseComputed(
"isMultiple",
"pollOptionsCount",
"isNumber",
"pollMin",
"pollMax"
)
minMaxValueValidation(
isMultiple,
pollOptionsCount,
isNumber,
pollMin,
pollMax
) {
pollMin = parseInt(pollMin, 10) || 0;
pollMax = parseInt(pollMax, 10) || 0;
const fail = {
failed: true,
reason: I18n.t("poll.ui_builder.help.invalid_values"),
};
if (isMultiple) {
if (
pollMin > pollMax ||
pollMin < 0 ||
(pollOptionsCount > 0 && pollMax > pollOptionsCount)
) {
return EmberObject.create(fail);
}
} else if (isNumber) {
if (pollMin >= pollMax) {
return EmberObject.create(fail);
}
}
return EmberObject.create({ ok: true });
},
@discourseComputed("isNumber", "pollStep")
minStepValueValidation(isNumber, pollStep) {
let options = { ok: true };
if (isNumber && pollStep < 1) {
options = {
failed: true,
reason: I18n.t("poll.ui_builder.help.min_step_value"),
};
}
return EmberObject.create(options);
},
@discourseComputed(
"minMaxValueValidation",
"minStepValueValidation",
"minNumOfOptionsValidation"
)
disableInsert(
minMaxValueValidation,
minStepValueValidation,
minNumOfOptionsValidation
) {
return (
!minMaxValueValidation.ok ||
!minStepValueValidation.ok ||
!minNumOfOptionsValidation.ok
);
},
_comboboxOptions(startIndex, endIndex) {
return [...Array(endIndex - startIndex).keys()].map((number) => ({
value: number + startIndex,
@ -377,29 +318,45 @@ export default Controller.extend({
}));
},
_setupPoll() {
this.setProperties({
pollType: this.get("pollTypes.firstObject.value"),
publicPoll: false,
pollOptions: "",
pollMin: 1,
pollMax: null,
pollStep: 1,
autoClose: false,
chartType: BAR_CHART_TYPE,
pollResult: this.alwaysPollResult,
pollGroups: null,
pollTitle: null,
date: moment().add(1, "day").format("YYYY-MM-DD"),
time: moment().add(1, "hour").format("HH:mm"),
});
@action
insertPoll() {
this.toolbarEvent.addText(this.pollOutput);
this.send("closeModal");
},
actions: {
insertPoll() {
this.toolbarEvent.addText(this.pollOutput);
this.send("closeModal");
this._setupPoll();
},
@action
toggleAdvanced() {
this.toggleProperty("showAdvanced");
},
@action
addOption(beforeOption, value, e) {
if (value !== "") {
const idx = this.pollOptions.indexOf(beforeOption) + 1;
const option = EmberObject.create({ value: "" });
this.pollOptions.insertAt(idx, option);
let lastOptionIdx = 0;
this.pollOptions.forEach((o) => o.set("idx", lastOptionIdx++));
next(() => {
const pollOptions = document.getElementsByClassName("poll-options");
if (pollOptions) {
const inputs = pollOptions[0].getElementsByTagName("input");
if (option.idx < inputs.length) {
inputs[option.idx].focus();
}
}
});
}
if (e) {
e.preventDefault();
}
},
@action
removeOption(option) {
this.pollOptions.removeObject(option);
},
});

View File

@ -1,129 +1,158 @@
{{#d-modal-body title="poll.ui_builder.title" class="poll-ui-builder"}}
<form class="poll-ui-builder-form form-horizontal">
<div class="options">
<div class="input-group poll-select">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_type.label"}}</label>
{{combo-box
content=pollTypes
value=pollType
valueProperty="value"
class="poll-type"
onChange=(action (mut pollType))
}}
</div>
<div class="input-group poll-type">
<a href {{action (mut pollType) "regular"}} class="poll-type-value {{if isRegular "active"}}">
{{i18n "poll.ui_builder.poll_type.regular"}}
</a>
<div class="input-group poll-select">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_result.label"}}</label>
{{combo-box
content=pollResults
value=pollResult
class="poll-result"
valueProperty="value"
onChange=(action (mut pollResult))
}}
</div>
<a href {{action (mut pollType) "multiple"}} class="poll-type-value {{if isMultiple "active"}}">
{{i18n "poll.ui_builder.poll_type.multiple"}}
</a>
<div class="input-group poll-select">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_groups.label"}}</label>
{{group-chooser
content=siteGroups
value=pollGroups
onChange=(action (mut pollGroups))
labelProperty="name"
valueProperty="name"}}
</div>
{{#if showNumber}}
<a href {{action (mut pollType) "number"}} class="poll-type-value {{if isNumber "active"}}">
{{i18n "poll.ui_builder.poll_type.number"}}
</a>
{{/if}}
</div>
{{#unless isNumber}}
<div class="input-group poll-select">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_chart_type.label"}}</label>
{{combo-box
class="poll-chart-type"
content=pollChartTypes
value=chartType
valueProperty="value"
onChange=(action (mut chartType))
}}
</div>
{{/unless}}
{{#if showAdvanced}}
<div class="input-group poll-title">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_title.label"}}</label>
{{input value=pollTitle}}
</div>
{{/if}}
{{#if showMinMax}}
<div class="input-group poll-number">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_config.min"}}</label>
{{input type="number"
value=pollMin
valueProperty="value"
class="poll-options-min"}}
</div>
{{input-tip validation=minMaxValueValidation}}
<div class="input-group poll-number">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_config.max"}}</label>
{{input type="number"
value=pollMax
valueProperty="value"
class="poll-options-max"}}
</div>
{{#if isNumber}}
<div class="input-group poll-number">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_config.step"}}</label>
{{input type="number"
value=pollStep
valueProperty="value"
min="1"
class="poll-options-step"}}
</div>
{{input-tip validation=minStepValueValidation}}
{{/if}}
{{#unless isNumber}}
<div class="poll-options">
{{#if showAdvanced}}
<label class="input-group-label">{{i18n "poll.ui_builder.poll_options.label"}}</label>
{{/if}}
<div class="input-group poll-title">
<label>{{i18n "poll.ui_builder.poll_title.label"}}</label>
{{input value=pollTitle}}
{{#each pollOptions as |option|}}
<div class="input-group poll-option-value">
{{input value=option.value enter=(action "addOption" option)}}
{{#if canRemoveOption}}
{{d-button icon="trash-alt" action=(action "removeOption" option)}}
{{/if}}
</div>
{{/each}}
<div class="poll-option-controls">
{{d-button icon="plus" label="poll.ui_builder.poll_options.add" action=(action "addOption" pollOptions.lastObject)}}
{{#if showMinNumOfOptionsValidation}}
{{input-tip validation=minNumOfOptionsValidation}}
{{/if}}
</div>
</div>
{{/unless}}
{{#unless isRegular}}
<div class="options">
<div class="input-group poll-number">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_config.min"}}</label>
{{input type="number"
value=pollMin
valueProperty="value"
class="poll-options-min"}}
</div>
{{#unless isNumber}}
<div class="input-group poll-textarea">
<label>{{i18n "poll.ui_builder.poll_options.label"}}</label>
{{textarea value=pollOptions autocomplete="discourse"}}
</div>
{{input-tip validation=minNumOfOptionsValidation}}
{{/unless}}
<div class="input-group poll-number">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_config.max"}}</label>
{{input type="number"
value=pollMax
valueProperty="value"
class="poll-options-max"}}
</div>
{{#unless isPie}}
<div class="input-group poll-checkbox">
<label>
{{input type="checkbox" checked=publicPoll}}
{{i18n "poll.ui_builder.poll_public.label"}}
</label>
{{#if isNumber}}
<div class="input-group poll-number">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_config.step"}}</label>
{{input type="number"
value=pollStep
valueProperty="value"
min="1"
class="poll-options-step"}}
</div>
{{/unless}}
{{input-tip validation=minStepValueValidation}}
{{/if}}
</div>
<div class="input-group poll-checkbox">
{{#unless minMaxValueValidation.ok}}
{{input-tip validation=minMaxValueValidation}}
{{/unless}}
{{/unless}}
{{#if showAdvanced}}
<div class="input-group poll-allowed-groups">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_groups.label"}}</label>
{{group-chooser
content=siteGroups
value=pollGroups
onChange=(action (mut pollGroups))
labelProperty="name"
valueProperty="name"}}
</div>
<div class="input-group poll-date">
<label class="input-group-label">{{i18n "poll.ui_builder.automatic_close.label"}}</label>
{{date-time-input date=pollAutoClose onChange=(action (mut pollAutoClose)) clearable=true}}
</div>
<div class="input-group poll-select">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_result.label"}}</label>
{{combo-box
content=pollResults
value=pollResult
class="poll-result"
valueProperty="value"
onChange=(action (mut pollResult))
}}
</div>
{{#unless isNumber}}
<div class="input-group poll-select column">
<label class="input-group-label">{{i18n "poll.ui_builder.poll_chart_type.label"}}</label>
<div class="radio-group">
{{radio-button id="poll-chart-type-bar" name="poll-chart-type" value="bar" selection=chartType}}
<label for="poll-chart-type-bar">{{d-icon "chart-bar"}} {{i18n "poll.ui_builder.poll_chart_type.bar"}}</label>
</div>
<div class="radio-group">
{{radio-button id="poll-chart-type-pie" name="poll-chart-type" value="pie" selection=chartType}}
<label for="poll-chart-type-pie">{{d-icon "chart-pie"}} {{i18n "poll.ui_builder.poll_chart_type.pie"}}</label>
</div>
</div>
{{/unless}}
{{#unless isPie}}
<div class="input-group poll-checkbox column">
<label>
{{input type="checkbox" checked=autoClose}}
{{i18n "poll.ui_builder.automatic_close.label"}}
{{input type="checkbox" checked=publicPoll}}
{{i18n "poll.ui_builder.poll_public.label"}}
</label>
</div>
{{#if autoClose}}
<div class="input-group poll-date">
{{date-picker-future value=date containerId="date-container"}}
{{input type="time" value=time}}
</div>
<div class="input-group poll-date">
<div id="date-container"></div>
</div>
{{/if}}
</div>
<div class="d-editor-preview">
{{cook-text this.pollOutput}}
</div>
</form>
{{/unless}}
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action=(action "insertPoll") icon="chart-bar" class="btn-primary" label="poll.ui_builder.insert" disabled=disableInsert}}
{{d-button
action=(action "insertPoll")
icon="chart-bar"
class="btn-primary"
label="poll.ui_builder.insert"
disabled=disableInsert}}
{{d-button
label="cancel"
class="btn-flat"
action=(route-action "closeModal")
}}
{{d-button
action=(action "toggleAdvanced")
class="show-advanced"
icon="cog"
title=(if showAdvanced "poll.ui_builder.hide_advanced" "poll.ui_builder.show_advanced")}}
</div>

View File

@ -1,86 +1,122 @@
$poll-margin: 10px;
.poll-ui-builder-form {
display: flex;
margin: 0;
.options {
flex-shrink: 0;
width: 280px;
}
.combo-box,
.multi-select {
min-width: 180px;
width: 180px;
}
.d-editor-preview {
margin-left: $poll-margin * 2;
width: 360px;
.poll-ui-builder-modal {
.show-advanced {
margin-left: auto;
margin-right: 0;
}
}
.poll-ui-builder {
label {
font-weight: bold;
display: inline;
}
.input-group {
margin-bottom: 15px;
input[type="number"],
input[type="text"],
input[type="time"],
.combo-box,
.multi-select,
.select-kit-header,
.date-picker-wrapper {
height: 34px;
margin: 0;
width: 100%;
}
button {
height: 34px;
margin-left: 5px;
}
.date-picker-wrapper {
margin-right: 5px;
}
}
.poll-checkbox {
label {
width: unset;
}
}
.radio-group {
display: inline-block;
margin-right: 30px;
input {
vertical-align: top;
}
label {
display: inline-block;
}
}
.poll-type {
display: block;
.poll-type-value {
border: none;
color: var(--primary);
display: inline-block;
padding: 6px 12px 3px;
&.active {
background-color: var(--quaternary);
color: var(--secondary);
}
}
}
.poll-options {
margin-bottom: 15px;
.poll-option-value {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 3px;
button {
margin-left: 3px;
}
}
.poll-option-controls {
margin-top: 10px;
}
.tip {
display: inline-block;
vertical-align: bottom;
}
}
.d-date-time-input {
border: 1px solid var(--primary-medium);
justify-content: space-between;
width: 100%;
.d-date-input {
width: 100%;
}
.d-time-input {
.select-kit-header {
min-width: 120px;
}
}
}
.tip {
display: block;
margin-bottom: $poll-margin * 1.5;
margin-top: $poll-margin / 2;
font-size: $font-down-1;
margin: -0.5em 0 0.5em;
}
.poll-select {
margin-bottom: $poll-margin;
display: flex;
align-items: center;
}
.input-group-label {
min-width: 85px;
}
.input-group {
display: flex;
justify-content: space-between;
}
.poll-number {
margin: $poll-margin 0;
display: flex;
align-items: center;
input {
width: 70px;
}
}
.poll-textarea,
.poll-title {
flex-direction: column;
}
.poll-title input {
.d-editor-preview {
width: 100%;
}
.poll-textarea textarea {
width: 100%;
height: 90px;
box-sizing: border-box;
}
.poll-select + .poll-title {
margin-top: $poll-margin;
}
.poll-textarea {
margin-top: $poll-margin;
}
.poll-checkbox,
.poll-date {
margin-top: $poll-margin / 2;
}
}

View File

@ -0,0 +1,32 @@
.poll-ui-builder-modal {
.modal-inner-container {
width: 600px;
}
.modal-body {
max-height: unset;
}
.poll-number {
margin-right: 10px;
&:last-of-type {
margin-right: 0;
}
}
.options {
display: flex;
justify-content: space-between;
}
.column {
display: inline-block;
width: calc(50% - 10px);
}
.d-editor-preview {
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid var(--primary-low);
}
}

View File

@ -1,5 +0,0 @@
.poll-ui-builder-form {
.d-editor-preview {
display: none;
}
}

View File

@ -95,15 +95,15 @@ en:
multiple: Multiple Choice
number: Number Rating
poll_result:
label: Results
label: Show Results...
always: Always visible
vote: On vote
closed: When closed
vote: Only after voting
closed: When the poll is closed
staff: Staff only
poll_groups:
label: Allowed groups
label: Limit voting to these groups
poll_chart_type:
label: Chart type
label: Result chart
bar: Bar
pie: Pie
poll_config:
@ -115,6 +115,9 @@ en:
poll_title:
label: Title (optional)
poll_options:
label: Enter one poll option per line
label: Options
add: Add option
automatic_close:
label: Automatically close poll
show_advanced: "Show Advanced Options"
hide_advanced: "Hide Advanced Options"

View File

@ -10,8 +10,8 @@ register_asset "stylesheets/common/poll.scss"
register_asset "stylesheets/common/poll-ui-builder.scss"
register_asset "stylesheets/common/poll-breakdown.scss"
register_asset "stylesheets/desktop/poll.scss", :desktop
register_asset "stylesheets/desktop/poll-ui-builder.scss", :desktop
register_asset "stylesheets/mobile/poll.scss", :mobile
register_asset "stylesheets/mobile/poll-ui-builder.scss", :mobile
register_svg_icon "far fa-check-square"

View File

@ -1,12 +1,10 @@
import {
acceptance,
exists,
queryAll,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer";
import { displayPollBuilderButton } from "discourse/plugins/poll/helpers/display-poll-builder-button";
import selectKit from "discourse/tests/helpers/select-kit-helper";
acceptance("Poll Builder - polls are enabled", function (needs) {
needs.user();
@ -48,22 +46,4 @@ acceptance("Poll Builder - polls are enabled", function (needs) {
"it shows the builder button"
);
});
test("poll preview", async function (assert) {
await displayPollBuilderButton();
const popupMenu = selectKit(".toolbar-popup-menu-options");
await popupMenu.selectRowByValue("showPollBuilder");
await fillIn(".poll-textarea textarea", "First option\nSecond option");
assert.equal(
queryAll(".d-editor-preview li:first-child").text(),
"First option"
);
assert.equal(
queryAll(".d-editor-preview li:last-child").text(),
"Second option"
);
});
});

View File

@ -1,10 +1,14 @@
import { controllerModule } from "discourse/tests/helpers/qunit-helpers";
import {
MULTIPLE_POLL_TYPE,
NUMBER_POLL_TYPE,
REGULAR_POLL_TYPE,
} from "discourse/plugins/poll/controllers/poll-ui-builder";
controllerModule("controller:poll-ui-builder", {
setupController(controller) {
controller.set("toolbarEvent", {
getText: () => "",
});
controller.set("toolbarEvent", { getText: () => "" });
controller.onShow();
},
needs: ["controller:modal"],
});
@ -13,17 +17,16 @@ test("isMultiple", function (assert) {
const controller = this.subject();
controller.setProperties({
pollType: controller.multiplePollType,
pollOptions: "a",
pollType: MULTIPLE_POLL_TYPE,
pollOptions: [{ value: "a" }],
});
assert.equal(controller.isMultiple, true, "it should be true");
controller.set("pollOptions", "");
assert.equal(controller.isMultiple, false, "it should be false");
controller.setProperties({ pollType: "random", pollOptions: "b" });
controller.setProperties({
pollType: "random",
pollOptions: [{ value: "b" }],
});
assert.equal(controller.isMultiple, false, "it should be false");
});
@ -31,170 +34,58 @@ test("isMultiple", function (assert) {
test("isNumber", function (assert) {
const controller = this.subject();
controller.set("pollType", controller.regularPollType);
controller.set("pollType", REGULAR_POLL_TYPE);
assert.equal(controller.isNumber, false, "it should be false");
controller.set("pollType", controller.numberPollType);
controller.set("pollType", NUMBER_POLL_TYPE);
assert.equal(controller.isNumber, true, "it should be true");
});
test("showMinMax", function (assert) {
const controller = this.subject();
controller.set("pollType", controller.numberPollType);
assert.equal(controller.showMinMax, true, "it should be true");
controller.set("pollType", controller.multiplePollType);
assert.equal(controller.showMinMax, true, "it should be true");
controller.set("pollType", controller.regularPollType);
assert.equal(controller.showMinMax, false, "it should be false");
});
test("pollOptionsCount", function (assert) {
const controller = this.subject();
controller.set("pollOptions", "1\n2\n");
controller.set("pollOptions", [{ value: "1" }, { value: "2" }]);
assert.equal(controller.pollOptionsCount, 2, "it should equal 2");
controller.set("pollOptions", "");
controller.set("pollOptions", []);
assert.equal(controller.pollOptionsCount, 0, "it should equal 0");
});
test("pollMinOptions", function (assert) {
const controller = this.subject();
controller.setProperties({
pollType: controller.multiplePollType,
pollOptions: "z",
});
assert.deepEqual(
controller.pollMinOptions,
[{ name: 1, value: 1 }],
"it should return the right options"
);
controller.set("pollOptions", "z\nx");
assert.deepEqual(
controller.pollMinOptions,
[
{ name: 1, value: 1 },
{ name: 2, value: 2 },
],
"it should return the right options"
);
controller.set("pollType", controller.numberPollType);
controller.siteSettings.poll_maximum_options = 2;
assert.deepEqual(
controller.pollMinOptions,
[
{ name: 1, value: 1 },
{ name: 2, value: 2 },
],
"it should return the right options"
);
});
test("pollMaxOptions", function (assert) {
const controller = this.subject();
controller.setProperties({
pollType: controller.multiplePollType,
pollOptions: "y",
pollMin: 1,
});
assert.deepEqual(
controller.pollMaxOptions,
[],
"it should return the right options"
);
controller.set("pollOptions", "x\ny");
assert.deepEqual(
controller.pollMaxOptions,
[{ name: 2, value: 2 }],
"it should return the right options"
);
controller.siteSettings.poll_maximum_options = 3;
controller.setProperties({
pollType: controller.get("numberPollType"),
pollStep: 2,
pollMin: 1,
});
assert.deepEqual(
controller.pollMaxOptions,
[
{ name: 2, value: 2 },
{ name: 3, value: 3 },
{ name: 4, value: 4 },
{ name: 5, value: 5 },
{ name: 6, value: 6 },
],
"it should return the right options"
);
});
test("pollStepOptions", function (assert) {
const controller = this.subject();
controller.siteSettings.poll_maximum_options = 3;
assert.equal(controller.pollStepOptions, null, "is should return null");
controller.set("pollType", controller.numberPollType);
assert.deepEqual(
controller.pollStepOptions,
[
{ name: 1, value: 1 },
{ name: 2, value: 2 },
{ name: 3, value: 3 },
],
"it should return the right options"
);
});
test("disableInsert", function (assert) {
const controller = this.subject();
controller.siteSettings.poll_maximum_options = 20;
assert.equal(controller.disableInsert, true, "it should be true");
controller.set("pollOptions", "a\nb");
controller.set("pollOptions", [{ value: "a" }, { value: "b" }]);
assert.equal(controller.disableInsert, false, "it should be false");
controller.set("pollType", controller.numberPollType);
controller.set("pollType", NUMBER_POLL_TYPE);
assert.equal(controller.disableInsert, false, "it should be false");
controller.setProperties({
pollType: controller.regularPollType,
pollOptions: "a\nb\nc",
pollType: REGULAR_POLL_TYPE,
pollOptions: [{ value: "a" }, { value: "b" }, { value: "c" }],
});
assert.equal(controller.disableInsert, false, "it should be false");
controller.setProperties({
pollType: controller.regularPollType,
pollOptions: "",
pollType: REGULAR_POLL_TYPE,
pollOptions: [],
});
assert.equal(controller.disableInsert, true, "it should be true");
controller.setProperties({
pollType: controller.regularPollType,
pollOptions: "w",
pollType: REGULAR_POLL_TYPE,
pollOptions: [{ value: "w" }],
});
assert.equal(controller.disableInsert, false, "it should be false");
@ -205,7 +96,7 @@ test("number pollOutput", function (assert) {
controller.siteSettings.poll_maximum_options = 20;
controller.setProperties({
pollType: controller.numberPollType,
pollType: NUMBER_POLL_TYPE,
pollMin: 1,
});
@ -244,10 +135,9 @@ test("regular pollOutput", function (assert) {
const controller = this.subject();
controller.siteSettings.poll_maximum_options = 20;
controller.set("pollOptions", "1\n2");
controller.setProperties({
pollOptions: "1\n2",
pollType: controller.regularPollType,
pollOptions: [{ value: "1" }, { value: "2" }],
pollType: REGULAR_POLL_TYPE,
});
assert.equal(
@ -278,9 +168,9 @@ test("multiple pollOutput", function (assert) {
controller.siteSettings.poll_maximum_options = 20;
controller.setProperties({
pollType: controller.multiplePollType,
pollType: MULTIPLE_POLL_TYPE,
pollMin: 1,
pollOptions: "\n\n1\n\n2",
pollOptions: [{ value: "1" }, { value: "2" }],
});
assert.equal(