From 773e198cb3d18150af59bfd590c9d44c975bfb21 Mon Sep 17 00:00:00 2001 From: David Taylor <david@taylorhq.com> Date: Tue, 4 Jul 2023 15:25:34 +0100 Subject: [PATCH] DEV: Convert poll modals to new component-based API (#22164) --- .../components/modal/poll-breakdown.hbs | 76 ++++++ .../modal}/poll-breakdown.js | 29 +- .../components/modal/poll-ui-builder.hbs | 251 ++++++++++++++++++ .../modal}/poll-ui-builder.js | 49 +--- .../components/poll-breakdown-chart.js | 2 +- .../initializers/add-poll-ui-builder.js | 9 +- .../templates/modal/poll-breakdown.hbs | 55 ---- .../templates/modal/poll-ui-builder.hbs | 244 ----------------- .../discourse/widgets/discourse-poll.js | 11 +- .../component/poll-ui-builder-test.js | 214 +++++++++++++++ .../unit/controllers/poll-ui-builder-test.js | 205 -------------- 11 files changed, 579 insertions(+), 566 deletions(-) create mode 100644 plugins/poll/assets/javascripts/discourse/components/modal/poll-breakdown.hbs rename plugins/poll/assets/javascripts/discourse/{controllers => components/modal}/poll-breakdown.js (88%) create mode 100644 plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs rename plugins/poll/assets/javascripts/discourse/{controllers => components/modal}/poll-ui-builder.js (90%) delete mode 100644 plugins/poll/assets/javascripts/discourse/templates/modal/poll-breakdown.hbs delete mode 100644 plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs create mode 100644 plugins/poll/test/javascripts/component/poll-ui-builder-test.js delete mode 100644 plugins/poll/test/javascripts/unit/controllers/poll-ui-builder-test.js diff --git a/plugins/poll/assets/javascripts/discourse/components/modal/poll-breakdown.hbs b/plugins/poll/assets/javascripts/discourse/components/modal/poll-breakdown.hbs new file mode 100644 index 00000000000..218405dd995 --- /dev/null +++ b/plugins/poll/assets/javascripts/discourse/components/modal/poll-breakdown.hbs @@ -0,0 +1,76 @@ +{{! template-lint-disable no-invalid-interactive }} +<DModal @title={{i18n "poll.breakdown.title"}} class="has-tabs"> + <:headerBelowTitle> + <ul class="modal-tabs"> + <li + class={{concat-class + "modal-tab percentage" + (if (eq this.displayMode "percentage") "active") + }} + {{on "click" (fn (mut this.displayMode) "percentage")}} + >{{i18n "poll.breakdown.percentage"}}</li> + <li + class={{concat-class + "modal-tab count" + (if (eq this.displayMode "count") "active") + }} + {{on "click" (fn (mut this.displayMode) "count")}} + >{{i18n "poll.breakdown.count"}}</li> + </ul> + </:headerBelowTitle> + <:body> + <div class="poll-breakdown-sidebar"> + <p class="poll-breakdown-title"> + {{this.title}} + </p> + + <div class="poll-breakdown-total-votes">{{i18n + "poll.breakdown.votes" + count=this.model.poll.voters + }}</div> + + <ul class="poll-breakdown-options"> + {{#each this.model.poll.options as |option index|}} + <PollBreakdownOption + @option={{option}} + @index={{index}} + @totalVotes={{this.totalVotes}} + @optionsCount={{this.model.poll.options.length}} + @displayMode={{this.displayMode}} + @highlightedOption={{this.highlightedOption}} + @onMouseOver={{fn (mut this.highlightedOption) index}} + @onMouseOut={{fn (mut this.highlightedOption) null}} + /> + {{/each}} + </ul> + </div> + + <div class="poll-breakdown-body"> + <div class="poll-breakdown-body-header"> + <label class="poll-breakdown-body-header-label">{{i18n + "poll.breakdown.breakdown" + }}</label> + + <ComboBox + @content={{this.groupableUserFields}} + @value={{this.groupedBy}} + @nameProperty="label" + @class="poll-breakdown-dropdown" + @onChange={{action this.setGrouping}} + /> + </div> + + <div class="poll-breakdown-charts"> + {{#each this.charts as |chart|}} + <PollBreakdownChart + @group={{get chart "group"}} + @options={{get chart "options"}} + @displayMode={{this.displayMode}} + @highlightedOption={{this.highlightedOption}} + @setHighlightedOption={{fn (mut this.highlightedOption)}} + /> + {{/each}} + </div> + </div> + </:body> +</DModal> \ No newline at end of file diff --git a/plugins/poll/assets/javascripts/discourse/controllers/poll-breakdown.js b/plugins/poll/assets/javascripts/discourse/components/modal/poll-breakdown.js similarity index 88% rename from plugins/poll/assets/javascripts/discourse/controllers/poll-breakdown.js rename to plugins/poll/assets/javascripts/discourse/components/modal/poll-breakdown.js index ba2e2514e8c..0cd13fa67fa 100644 --- a/plugins/poll/assets/javascripts/discourse/controllers/poll-breakdown.js +++ b/plugins/poll/assets/javascripts/discourse/components/modal/poll-breakdown.js @@ -1,7 +1,6 @@ import { inject as service } from "@ember/service"; -import Controller from "@ember/controller"; +import Component from "@ember/component"; import I18n from "I18n"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; import { action } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import { classify } from "@ember/string"; @@ -10,9 +9,7 @@ import { htmlSafe } from "@ember/template"; import loadScript from "discourse/lib/load-script"; import { popupAjaxError } from "discourse/lib/ajax-error"; -export default class PollBreakdownController extends Controller.extend( - ModalFunctionality -) { +export default class PollBreakdownModal extends Component { @service dialog; model = null; @@ -21,6 +18,16 @@ export default class PollBreakdownController extends Controller.extend( highlightedOption = null; displayMode = "percentage"; + init() { + this.set("groupedBy", this.model.groupableUserFields[0]); + loadScript("/javascripts/Chart.min.js") + .then(() => loadScript("/javascripts/chartjs-plugin-datalabels.min.js")) + .then(() => { + this.fetchGroupedPollData(); + }); + super.init(...arguments); + } + @discourseComputed("model.poll.title", "model.post.topic.title") title(pollTitle, topicTitle) { return pollTitle ? htmlSafe(pollTitle) : topicTitle; @@ -44,18 +51,6 @@ export default class PollBreakdownController extends Controller.extend( return options.reduce((sum, option) => sum + option.votes, 0); } - onShow() { - this.set("charts", null); - this.set("displayMode", "percentage"); - this.set("groupedBy", this.model.groupableUserFields[0]); - - loadScript("/javascripts/Chart.min.js") - .then(() => loadScript("/javascripts/chartjs-plugin-datalabels.min.js")) - .then(() => { - this.fetchGroupedPollData(); - }); - } - fetchGroupedPollData() { return ajax("/polls/grouped_poll_results.json", { data: { diff --git a/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs b/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs new file mode 100644 index 00000000000..0955f70b3ff --- /dev/null +++ b/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs @@ -0,0 +1,251 @@ +<DModal + @title={{i18n "poll.ui_builder.title"}} + @closeModal={{@closeModal}} + @inline={{@inline}} + class="poll-ui-builder" +> + <:body> + <div class="input-group poll-type"> + <a + href + {{on "click" (fn this.updatePollType "regular")}} + class="poll-type-value poll-type-value-regular + {{if this.isRegular 'active'}}" + > + {{i18n "poll.ui_builder.poll_type.regular"}} + </a> + + <a + href + {{on "click" (fn this.updatePollType "multiple")}} + class="poll-type-value poll-type-value-multiple + {{if this.isMultiple 'active'}}" + > + {{i18n "poll.ui_builder.poll_type.multiple"}} + </a> + + {{#if this.showNumber}} + <a + href + {{on "click" (fn this.updatePollType "number")}} + class="poll-type-value poll-type-value-number + {{if this.isNumber 'active'}}" + > + {{i18n "poll.ui_builder.poll_type.number"}} + </a> + {{/if}} + </div> + + {{#if this.showAdvanced}} + <div class="input-group poll-title"> + <label class="input-group-label">{{i18n + "poll.ui_builder.poll_title.label" + }}</label> + <Input @value={{this.pollTitle}} /> + </div> + {{/if}} + + {{#unless this.isNumber}} + <div class="poll-options"> + {{#if this.showAdvanced}} + <label class="input-group-label">{{i18n + "poll.ui_builder.poll_options.label" + }}</label> + <Textarea + @value={{this.pollOptionsText}} + {{on "input" (action "onOptionsTextChange")}} + /> + {{#if this.showMinNumOfOptionsValidation}} + {{#unless this.minNumOfOptionsValidation.ok}} + <InputTip @validation={{this.minNumOfOptionsValidation}} /> + {{/unless}} + {{/if}} + {{else}} + {{#each this.pollOptions as |option|}} + <div class="input-group poll-option-value"> + <Input + @value={{option.value}} + @enter={{action "addOption" option}} + /> + {{#if this.canRemoveOption}} + <DButton + @icon="trash-alt" + @action={{action "removeOption" option}} + /> + {{/if}} + </div> + {{/each}} + + <div class="poll-option-controls"> + <DButton + class="btn-default poll-option-add" + @icon="plus" + @label="poll.ui_builder.poll_options.add" + @action={{action "addOption" this.pollOptions.lastObject}} + /> + {{#if + (and + this.showMinNumOfOptionsValidation + (not this.minNumOfOptionsValidation.ok) + ) + }} + <InputTip @validation={{this.minNumOfOptionsValidation}} /> + {{/if}} + </div> + {{/if}} + </div> + {{/unless}} + + {{#unless this.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={{this.pollMin}} + class="poll-options-min" + min="1" + /> + </div> + + <div class="input-group poll-number"> + <label class="input-group-label">{{i18n + "poll.ui_builder.poll_config.max" + }}</label> + <Input + @type="number" + @value={{this.pollMax}} + class="poll-options-max" + min="1" + /> + </div> + + {{#if this.isNumber}} + <div class="input-group poll-number"> + <label class="input-group-label">{{i18n + "poll.ui_builder.poll_config.step" + }}</label> + <Input + @type="number" + @value={{this.pollStep}} + min="1" + class="poll-options-step" + /> + </div> + {{/if}} + </div> + + {{#unless this.minMaxValueValidation.ok}} + <InputTip @validation={{this.minMaxValueValidation}} /> + {{/unless}} + {{/unless}} + + {{#if this.showAdvanced}} + <div class="input-group poll-allowed-groups"> + <label class="input-group-label">{{i18n + "poll.ui_builder.poll_groups.label" + }}</label> + <GroupChooser + @content={{this.siteGroups}} + @value={{this.pollGroups}} + @onChange={{action (mut this.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> + <DateTimeInput + @date={{this.pollAutoClose}} + @onChange={{action (mut this.pollAutoClose)}} + @clearable={{true}} + @useGlobalPickerContainer={{true}} + /> + </div> + + <div class="input-group poll-select"> + <label class="input-group-label">{{i18n + "poll.ui_builder.poll_result.label" + }}</label> + <ComboBox + @content={{this.pollResults}} + @value={{this.pollResult}} + @class="poll-result" + @valueProperty="value" + @onChange={{action (mut this.pollResult)}} + /> + </div> + + {{#unless this.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"> + <RadioButton + @id="poll-chart-type-bar" + @name="poll-chart-type" + @value="bar" + @selection={{this.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"> + <RadioButton + @id="poll-chart-type-pie" + @name="poll-chart-type" + @value="pie" + @selection={{this.chartType}} + /> + <label for="poll-chart-type-pie">{{d-icon "chart-pie"}} + {{i18n "poll.ui_builder.poll_chart_type.pie"}}</label> + </div> + </div> + {{/unless}} + + {{#unless this.isPie}} + <div class="input-group poll-checkbox column"> + <label> + <Input + @type="checkbox" + @checked={{this.publicPoll}} + class="poll-toggle-public" + /> + {{i18n "poll.ui_builder.poll_public.label"}} + </label> + </div> + {{/unless}} + {{/if}} + </:body> + <:footer> + <DButton + @action={{action "insertPoll"}} + @icon="chart-bar" + class="btn-primary insert-poll" + @label="poll.ui_builder.insert" + @disabled={{this.disableInsert}} + /> + + <DButton @label="cancel" @class="btn-flat" @action={{@closeModal}} /> + + <DButton + @action={{action "toggleAdvanced"}} + class="btn-default show-advanced" + @icon="cog" + @title={{if + this.showAdvanced + "poll.ui_builder.hide_advanced" + "poll.ui_builder.show_advanced" + }} + /> + + </:footer> +</DModal> \ No newline at end of file diff --git a/plugins/poll/assets/javascripts/discourse/controllers/poll-ui-builder.js b/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.js similarity index 90% rename from plugins/poll/assets/javascripts/discourse/controllers/poll-ui-builder.js rename to plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.js index df707bac268..f05f3f5ece9 100644 --- a/plugins/poll/assets/javascripts/discourse/controllers/poll-ui-builder.js +++ b/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.js @@ -1,10 +1,9 @@ import { gt, or } from "@ember/object/computed"; -import Controller from "@ember/controller"; +import Component from "@ember/component"; import EmberObject, { action } from "@ember/object"; import { next } from "@ember/runloop"; import discourseComputed from "discourse-common/utils/decorators"; import { observes } from "@ember-decorators/object"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; import I18n from "I18n"; export const BAR_CHART_TYPE = "bar"; @@ -19,46 +18,26 @@ const VOTE_POLL_RESULT = "on_vote"; const CLOSED_POLL_RESULT = "on_close"; const STAFF_POLL_RESULT = "staff_only"; -export default class PollUiBuilderController extends Controller.extend( - ModalFunctionality -) { +export default class PollUiBuilderModal extends Component { showAdvanced = false; pollType = REGULAR_POLL_TYPE; - pollTitle = ""; - pollOptions = null; - pollOptionsText = null; + pollTitle; + pollOptions = [EmberObject.create({ value: "" })]; + pollOptionsText = ""; pollMin = 1; pollMax = 2; pollStep = 1; - pollGroups = null; - pollAutoClose = null; + pollGroups; + pollAutoClose; pollResult = ALWAYS_POLL_RESULT; chartType = BAR_CHART_TYPE; - publicPoll = null; + publicPoll = false; @or("showAdvanced", "isNumber") showNumber; @gt("pollOptions.length", 1) canRemoveOption; - onShow() { - this.setProperties({ - showAdvanced: false, - pollType: REGULAR_POLL_TYPE, - pollTitle: null, - pollOptions: [EmberObject.create({ value: "" })], - pollOptionsText: "", - pollMin: 1, - pollMax: 2, - pollStep: 1, - pollGroups: null, - pollAutoClose: null, - pollResult: ALWAYS_POLL_RESULT, - chartType: BAR_CHART_TYPE, - publicPoll: false, - }); - } - - @discourseComputed - pollResults() { + @discourseComputed("currentUser.staff") + pollResults(staff) { const options = [ { name: I18n.t("poll.ui_builder.poll_result.always"), @@ -74,7 +53,7 @@ export default class PollUiBuilderController extends Controller.extend( }, ]; - if (this.get("currentUser.staff")) { + if (staff) { options.push({ name: I18n.t("poll.ui_builder.poll_result.staff"), value: STAFF_POLL_RESULT, @@ -168,7 +147,7 @@ export default class PollUiBuilderController extends Controller.extend( let pollHeader = "[poll"; let output = ""; - const match = this.toolbarEvent + const match = this.model.toolbarEvent .getText() .match(/\[poll(\s+name=[^\s\]]+)*.*\]/gim); @@ -354,8 +333,8 @@ export default class PollUiBuilderController extends Controller.extend( @action insertPoll() { - this.toolbarEvent.addText(this.pollOutput); - this.send("closeModal"); + this.model.toolbarEvent.addText(this.pollOutput); + this.closeModal(); } @action diff --git a/plugins/poll/assets/javascripts/discourse/components/poll-breakdown-chart.js b/plugins/poll/assets/javascripts/discourse/components/poll-breakdown-chart.js index 72d5c0ab824..17d0aa6932d 100644 --- a/plugins/poll/assets/javascripts/discourse/components/poll-breakdown-chart.js +++ b/plugins/poll/assets/javascripts/discourse/components/poll-breakdown-chart.js @@ -2,7 +2,7 @@ import { classNames } from "@ember-decorators/component"; import { mapBy } from "@ember/object/computed"; import Component from "@ember/component"; import I18n from "I18n"; -import { PIE_CHART_TYPE } from "../controllers/poll-ui-builder"; +import { PIE_CHART_TYPE } from "../components/modal/poll-ui-builder"; import discourseComputed from "discourse-common/utils/decorators"; import { getColors } from "discourse/plugins/poll/lib/chart-colors"; import { htmlSafe } from "@ember/template"; diff --git a/plugins/poll/assets/javascripts/discourse/initializers/add-poll-ui-builder.js b/plugins/poll/assets/javascripts/discourse/initializers/add-poll-ui-builder.js index ab5c59cfeae..41dea414b41 100644 --- a/plugins/poll/assets/javascripts/discourse/initializers/add-poll-ui-builder.js +++ b/plugins/poll/assets/javascripts/discourse/initializers/add-poll-ui-builder.js @@ -1,6 +1,7 @@ import discourseComputed from "discourse-common/utils/decorators"; -import showModal from "discourse/lib/show-modal"; import { withPluginApi } from "discourse/lib/plugin-api"; +import PollUiBuilder from "../components/modal/poll-ui-builder"; +import { getOwner } from "@ember/application"; function initializePollUIBuilder(api) { api.modifyClass("controller:composer", { @@ -22,7 +23,11 @@ function initializePollUIBuilder(api) { actions: { showPollBuilder() { - showModal("poll-ui-builder").set("toolbarEvent", this.toolbarEvent); + getOwner(this) + .lookup("service:modal") + .show(PollUiBuilder, { + model: { toolbarEvent: this.toolbarEvent }, + }); }, }, }); diff --git a/plugins/poll/assets/javascripts/discourse/templates/modal/poll-breakdown.hbs b/plugins/poll/assets/javascripts/discourse/templates/modal/poll-breakdown.hbs deleted file mode 100644 index 4753fa89d56..00000000000 --- a/plugins/poll/assets/javascripts/discourse/templates/modal/poll-breakdown.hbs +++ /dev/null @@ -1,55 +0,0 @@ -<DModalBody @title="poll.breakdown.title"> - <div class="poll-breakdown-sidebar"> - <p class="poll-breakdown-title"> - {{this.title}} - </p> - - <div class="poll-breakdown-total-votes">{{i18n - "poll.breakdown.votes" - count=this.model.poll.voters - }}</div> - - <ul class="poll-breakdown-options"> - {{#each this.model.poll.options as |option index|}} - <PollBreakdownOption - @option={{option}} - @index={{index}} - @totalVotes={{this.totalVotes}} - @optionsCount={{this.model.poll.options.length}} - @displayMode={{this.displayMode}} - @highlightedOption={{this.highlightedOption}} - @onMouseOver={{fn (mut this.highlightedOption) index}} - @onMouseOut={{fn (mut this.highlightedOption) null}} - /> - {{/each}} - </ul> - </div> - - <div class="poll-breakdown-body"> - <div class="poll-breakdown-body-header"> - <label class="poll-breakdown-body-header-label">{{i18n - "poll.breakdown.breakdown" - }}</label> - - <ComboBox - @content={{this.groupableUserFields}} - @value={{this.groupedBy}} - @nameProperty="label" - @class="poll-breakdown-dropdown" - @onChange={{action this.setGrouping}} - /> - </div> - - <div class="poll-breakdown-charts"> - {{#each this.charts as |chart|}} - <PollBreakdownChart - @group={{get chart "group"}} - @options={{get chart "options"}} - @displayMode={{this.displayMode}} - @highlightedOption={{this.highlightedOption}} - @setHighlightedOption={{fn (mut this.highlightedOption)}} - /> - {{/each}} - </div> - </div> -</DModalBody> \ No newline at end of file diff --git a/plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs b/plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs deleted file mode 100644 index 41a2fff5d05..00000000000 --- a/plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs +++ /dev/null @@ -1,244 +0,0 @@ -<DModalBody @title="poll.ui_builder.title" @class="poll-ui-builder"> - <div class="input-group poll-type"> - <a - href - {{on "click" (fn this.updatePollType "regular")}} - class="poll-type-value poll-type-value-regular - {{if this.isRegular 'active'}}" - > - {{i18n "poll.ui_builder.poll_type.regular"}} - </a> - - <a - href - {{on "click" (fn this.updatePollType "multiple")}} - class="poll-type-value poll-type-value-multiple - {{if this.isMultiple 'active'}}" - > - {{i18n "poll.ui_builder.poll_type.multiple"}} - </a> - - {{#if this.showNumber}} - <a - href - {{on "click" (fn this.updatePollType "number")}} - class="poll-type-value poll-type-value-number - {{if this.isNumber 'active'}}" - > - {{i18n "poll.ui_builder.poll_type.number"}} - </a> - {{/if}} - </div> - - {{#if this.showAdvanced}} - <div class="input-group poll-title"> - <label class="input-group-label">{{i18n - "poll.ui_builder.poll_title.label" - }}</label> - <Input @value={{this.pollTitle}} /> - </div> - {{/if}} - - {{#unless this.isNumber}} - <div class="poll-options"> - {{#if this.showAdvanced}} - <label class="input-group-label">{{i18n - "poll.ui_builder.poll_options.label" - }}</label> - <Textarea - @value={{this.pollOptionsText}} - {{on "input" (action "onOptionsTextChange")}} - /> - {{#if this.showMinNumOfOptionsValidation}} - {{#unless this.minNumOfOptionsValidation.ok}} - <InputTip @validation={{this.minNumOfOptionsValidation}} /> - {{/unless}} - {{/if}} - {{else}} - {{#each this.pollOptions as |option|}} - <div class="input-group poll-option-value"> - <Input - @value={{option.value}} - @enter={{action "addOption" option}} - /> - {{#if this.canRemoveOption}} - <DButton - @icon="trash-alt" - @action={{action "removeOption" option}} - /> - {{/if}} - </div> - {{/each}} - - <div class="poll-option-controls"> - <DButton - @class="btn-default" - @icon="plus" - @label="poll.ui_builder.poll_options.add" - @action={{action "addOption" this.pollOptions.lastObject}} - /> - {{#if - (and - this.showMinNumOfOptionsValidation - (not this.minNumOfOptionsValidation.ok) - ) - }} - <InputTip @validation={{this.minNumOfOptionsValidation}} /> - {{/if}} - </div> - {{/if}} - </div> - {{/unless}} - - {{#unless this.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={{this.pollMin}} - class="poll-options-min" - min="1" - /> - </div> - - <div class="input-group poll-number"> - <label class="input-group-label">{{i18n - "poll.ui_builder.poll_config.max" - }}</label> - <Input - @type="number" - @value={{this.pollMax}} - class="poll-options-max" - min="1" - /> - </div> - - {{#if this.isNumber}} - <div class="input-group poll-number"> - <label class="input-group-label">{{i18n - "poll.ui_builder.poll_config.step" - }}</label> - <Input - @type="number" - @value={{this.pollStep}} - min="1" - class="poll-options-step" - /> - </div> - {{/if}} - </div> - - {{#unless this.minMaxValueValidation.ok}} - <InputTip @validation={{this.minMaxValueValidation}} /> - {{/unless}} - {{/unless}} - - {{#if this.showAdvanced}} - <div class="input-group poll-allowed-groups"> - <label class="input-group-label">{{i18n - "poll.ui_builder.poll_groups.label" - }}</label> - <GroupChooser - @content={{this.siteGroups}} - @value={{this.pollGroups}} - @onChange={{action (mut this.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> - <DateTimeInput - @date={{this.pollAutoClose}} - @onChange={{action (mut this.pollAutoClose)}} - @clearable={{true}} - @useGlobalPickerContainer={{true}} - /> - </div> - - <div class="input-group poll-select"> - <label class="input-group-label">{{i18n - "poll.ui_builder.poll_result.label" - }}</label> - <ComboBox - @content={{this.pollResults}} - @value={{this.pollResult}} - @class="poll-result" - @valueProperty="value" - @onChange={{action (mut this.pollResult)}} - /> - </div> - - {{#unless this.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"> - <RadioButton - @id="poll-chart-type-bar" - @name="poll-chart-type" - @value="bar" - @selection={{this.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"> - <RadioButton - @id="poll-chart-type-pie" - @name="poll-chart-type" - @value="pie" - @selection={{this.chartType}} - /> - <label for="poll-chart-type-pie">{{d-icon "chart-pie"}} - {{i18n "poll.ui_builder.poll_chart_type.pie"}}</label> - </div> - </div> - {{/unless}} - - {{#unless this.isPie}} - <div class="input-group poll-checkbox column"> - <label> - <Input @type="checkbox" @checked={{this.publicPoll}} /> - {{i18n "poll.ui_builder.poll_public.label"}} - </label> - </div> - {{/unless}} - {{/if}} -</DModalBody> - -<div class="modal-footer"> - <DButton - @action={{action "insertPoll"}} - @icon="chart-bar" - @class="btn-primary" - @label="poll.ui_builder.insert" - @disabled={{this.disableInsert}} - /> - - <DButton - @label="cancel" - @class="btn-flat" - @action={{route-action "closeModal"}} - /> - - <DButton - @action={{action "toggleAdvanced"}} - @class="btn-default show-advanced" - @icon="cog" - @title={{if - this.showAdvanced - "poll.ui_builder.hide_advanced" - "poll.ui_builder.show_advanced" - }} - /> -</div> \ No newline at end of file diff --git a/plugins/poll/assets/javascripts/discourse/widgets/discourse-poll.js b/plugins/poll/assets/javascripts/discourse/widgets/discourse-poll.js index e31cd53cf1a..012ec03fdbf 100644 --- a/plugins/poll/assets/javascripts/discourse/widgets/discourse-poll.js +++ b/plugins/poll/assets/javascripts/discourse/widgets/discourse-poll.js @@ -1,5 +1,5 @@ import I18n from "I18n"; -import { PIE_CHART_TYPE } from "../controllers/poll-ui-builder"; +import { PIE_CHART_TYPE } from "../components/modal/poll-ui-builder"; import RawHtml from "discourse/widgets/raw-html"; import { ajax } from "discourse/lib/ajax"; import { avatarFor } from "discourse/widgets/post"; @@ -12,8 +12,9 @@ import loadScript from "discourse/lib/load-script"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { relativeAge } from "discourse/lib/formatter"; import round from "discourse/lib/round"; -import showModal from "discourse/lib/show-modal"; import { applyLocalDates } from "discourse/lib/local-dates"; +import PollBreakdownModal from "../components/modal/poll-breakdown"; +import { getOwner } from "@ember/application"; const FETCH_VOTERS_COUNT = 25; @@ -1070,12 +1071,8 @@ export default createWidget("discourse-poll", { }, showBreakdown() { - showModal("poll-breakdown", { + getOwner(this).lookup("service:modal").show(PollBreakdownModal, { model: this.attrs, - panels: [ - { id: "percentage", title: "poll.breakdown.percentage" }, - { id: "count", title: "poll.breakdown.count" }, - ], }); }, }); diff --git a/plugins/poll/test/javascripts/component/poll-ui-builder-test.js b/plugins/poll/test/javascripts/component/poll-ui-builder-test.js new file mode 100644 index 00000000000..79fa2681804 --- /dev/null +++ b/plugins/poll/test/javascripts/component/poll-ui-builder-test.js @@ -0,0 +1,214 @@ +import { module, test } from "qunit"; +import { click, fillIn, render } from "@ember/test-helpers"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import hbs from "htmlbars-inline-precompile"; +import selectKit from "discourse/tests/helpers/select-kit-helper"; + +async function setupBuilder(context) { + const results = []; + const model = { + toolbarEvent: { getText: () => "", addText: (t) => results.push(t) }, + }; + context.model = model; + await render( + hbs`<Modal::PollUiBuilder @inline={{true}} @model={{this.model}} @closeModal={{fn (mut this.closeCalled) true}} />` + ); + return results; +} + +module("Poll | Component | poll-ui-builder", function (hooks) { + setupRenderingTest(hooks); + + test("Can switch poll type", async function (assert) { + await setupBuilder(this); + + assert.dom(".poll-type-value-regular").hasClass("active"); + + await click(".poll-type-value-multiple"); + assert + .dom(".poll-type-value-multiple") + .hasClass("active", "can switch to 'multiple' type"); + + assert + .dom(".poll-type-value-number") + .doesNotExist("number type is hidden by default"); + + await click(".show-advanced"); + assert + .dom(".poll-type-value-number") + .exists("number type appears in advanced mode"); + + await click(".poll-type-value-number"); + assert + .dom(".poll-type-value-number") + .hasClass("active", "can switch to 'number' type"); + }); + + test("Automatically updates min/max when number of options change", async function (assert) { + await setupBuilder(this); + + await click(".poll-type-value-multiple"); + assert.dom(".poll-options-min").hasValue("0"); + assert.dom(".poll-options-max").hasValue("0"); + + await fillIn(".poll-option-value input", "a"); + assert.dom(".poll-options-min").hasValue("1"); + assert.dom(".poll-options-max").hasValue("1"); + + await click(".poll-option-add"); + + await fillIn(".poll-option-value:nth-of-type(2) input", "b"); + assert.dom(".poll-options-min").hasValue("1"); + assert.dom(".poll-options-max").hasValue("2"); + }); + + test("disables save button", async function (assert) { + this.siteSettings.poll_maximum_options = 3; + + await setupBuilder(this); + assert + .dom(".insert-poll") + .isDisabled("Insert button disabled when no options specified"); + + await fillIn(".poll-option-value input", "a"); + assert + .dom(".insert-poll") + .isEnabled("Insert button enabled once an option is specified"); + + await click(".poll-option-add"); + await fillIn(".poll-option-value:nth-of-type(2) input", "b"); + await click(".poll-option-add"); + await fillIn(".poll-option-value:nth-of-type(3) input", "c"); + await click(".poll-option-add"); + await fillIn(".poll-option-value:nth-of-type(4) input", "d"); + + assert + .dom(".insert-poll") + .isDisabled("Insert button disabled when too many options"); + }); + + test("number mode", async function (assert) { + const results = await setupBuilder(this); + + await click(".show-advanced"); + await click(".poll-type-value-number"); + + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=number results=always min=1 max=20 step=1]\n[/poll]\n" + ); + + await fillIn(".poll-options-step", "2"); + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=number results=always min=1 max=20 step=2]\n[/poll]\n", + "includes step value" + ); + + await click(".poll-toggle-public"); + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=number results=always min=1 max=20 step=2 public=true]\n[/poll]\n", + "includes public boolean" + ); + + await fillIn(".poll-options-step", "0"); + assert + .dom(".insert-poll") + .isDisabled("Insert button disabled when step is 0"); + }); + + test("regular mode", async function (assert) { + const results = await setupBuilder(this); + + await fillIn(".poll-option-value input", "a"); + await click(".poll-option-add"); + await fillIn(".poll-option-value:nth-of-type(2) input", "b"); + + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=regular results=always chartType=bar]\n* a\n* b\n[/poll]\n", + "has correct output" + ); + + await click(".show-advanced"); + + await click(".poll-toggle-public"); + + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=regular results=always public=true chartType=bar]\n* a\n* b\n[/poll]\n", + "has public boolean" + ); + + const groupChooser = selectKit(".group-chooser"); + await groupChooser.expand(); + await groupChooser.selectRowByName("custom_group"); + await groupChooser.collapse(); + + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=regular results=always public=true chartType=bar groups=custom_group]\n* a\n* b\n[/poll]\n", + "has groups" + ); + }); + + test("multi-choice mode", async function (assert) { + const results = await setupBuilder(this); + + await click(".poll-type-value-multiple"); + + await fillIn(".poll-option-value input", "a"); + await click(".poll-option-add"); + await fillIn(".poll-option-value:nth-of-type(2) input", "b"); + + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=multiple results=always min=1 max=2 chartType=bar]\n* a\n* b\n[/poll]\n", + "has correct output" + ); + + await click(".show-advanced"); + + await click(".poll-toggle-public"); + + await click(".insert-poll"); + assert.strictEqual( + results[results.length - 1], + "[poll type=multiple results=always min=1 max=2 public=true chartType=bar]\n* a\n* b\n[/poll]\n", + "has public boolean" + ); + }); + + test("staff_only option is not present for non-staff", async function (assert) { + await setupBuilder(this); + + await click(".show-advanced"); + const resultVisibility = selectKit(".poll-result"); + + assert.strictEqual(resultVisibility.header().value(), "always"); + + await resultVisibility.expand(); + assert.false( + resultVisibility.rowByValue("staff_only").exists(), + "staff_only is not visible to normal users" + ); + await resultVisibility.collapse(); + + this.currentUser.setProperties({ admin: true }); + + await resultVisibility.expand(); + assert.true( + resultVisibility.rowByValue("staff_only").exists(), + "staff_only is visible to staff" + ); + await resultVisibility.collapse(); + }); +}); diff --git a/plugins/poll/test/javascripts/unit/controllers/poll-ui-builder-test.js b/plugins/poll/test/javascripts/unit/controllers/poll-ui-builder-test.js deleted file mode 100644 index 5a810bb5e94..00000000000 --- a/plugins/poll/test/javascripts/unit/controllers/poll-ui-builder-test.js +++ /dev/null @@ -1,205 +0,0 @@ -import { module, test } from "qunit"; -import { setupTest } from "ember-qunit"; -import { - MULTIPLE_POLL_TYPE, - NUMBER_POLL_TYPE, - REGULAR_POLL_TYPE, -} from "discourse/plugins/poll/discourse/controllers/poll-ui-builder"; -import { settled } from "@ember/test-helpers"; - -function setupController(ctx) { - const controller = ctx.owner.lookup("controller:poll-ui-builder"); - controller.set("toolbarEvent", { getText: () => "" }); - controller.onShow(); - return controller; -} - -module("Unit | Controller | poll-ui-builder", function (hooks) { - setupTest(hooks); - - test("isMultiple", function (assert) { - const controller = setupController(this); - - controller.setProperties({ - pollType: MULTIPLE_POLL_TYPE, - pollOptions: [{ value: "a" }], - }); - assert.strictEqual(controller.isMultiple, true, "it should be true"); - - controller.setProperties({ - pollType: "random", - pollOptions: [{ value: "b" }], - }); - assert.strictEqual(controller.isMultiple, false, "it should be false"); - }); - - test("isNumber", function (assert) { - const controller = setupController(this); - - controller.set("pollType", REGULAR_POLL_TYPE); - assert.strictEqual(controller.isNumber, false, "it should be false"); - - controller.set("pollType", NUMBER_POLL_TYPE); - assert.strictEqual(controller.isNumber, true, "it should be true"); - }); - - test("pollOptionsCount", function (assert) { - const controller = setupController(this); - - controller.set("pollOptions", [{ value: "1" }, { value: "2" }]); - assert.strictEqual(controller.pollOptionsCount, 2, "it should equal 2"); - - controller.set("pollOptions", []); - assert.strictEqual(controller.pollOptionsCount, 0, "it should equal 0"); - }); - - test("disableInsert", function (assert) { - const controller = setupController(this); - - controller.siteSettings.poll_maximum_options = 20; - assert.strictEqual(controller.disableInsert, true, "it should be true"); - - controller.set("pollOptions", [{ value: "a" }, { value: "b" }]); - assert.strictEqual(controller.disableInsert, false, "it should be false"); - - controller.set("pollType", NUMBER_POLL_TYPE); - assert.strictEqual(controller.disableInsert, false, "it should be false"); - - controller.setProperties({ - pollType: REGULAR_POLL_TYPE, - pollOptions: [{ value: "a" }, { value: "b" }, { value: "c" }], - }); - assert.strictEqual(controller.disableInsert, false, "it should be false"); - - controller.setProperties({ - pollType: REGULAR_POLL_TYPE, - pollOptions: [], - }); - assert.strictEqual(controller.disableInsert, true, "it should be true"); - - controller.setProperties({ - pollType: REGULAR_POLL_TYPE, - pollOptions: [{ value: "w" }], - }); - assert.strictEqual(controller.disableInsert, false, "it should be false"); - }); - - test("number pollOutput", async function (assert) { - const controller = setupController(this); - controller.siteSettings.poll_maximum_options = 20; - - controller.setProperties({ - pollType: NUMBER_POLL_TYPE, - pollMin: 1, - }); - await settled(); - assert.strictEqual( - controller.pollOutput, - "[poll type=number results=always min=1 max=20 step=1]\n[/poll]\n", - "it should return the right output" - ); - - controller.set("pollStep", 2); - await settled(); - assert.strictEqual( - controller.pollOutput, - "[poll type=number results=always min=1 max=20 step=2]\n[/poll]\n", - "it should return the right output" - ); - - controller.set("publicPoll", true); - assert.strictEqual( - controller.pollOutput, - "[poll type=number results=always min=1 max=20 step=2 public=true]\n[/poll]\n", - "it should return the right output" - ); - - controller.set("pollStep", 0); - assert.strictEqual( - controller.pollOutput, - "[poll type=number results=always min=1 max=20 step=1 public=true]\n[/poll]\n", - "it should return the right output" - ); - }); - - test("regular pollOutput", function (assert) { - const controller = setupController(this); - controller.siteSettings.poll_maximum_options = 20; - - controller.setProperties({ - pollOptions: [{ value: "1" }, { value: "2" }], - pollType: REGULAR_POLL_TYPE, - }); - assert.strictEqual( - controller.pollOutput, - "[poll type=regular results=always chartType=bar]\n* 1\n* 2\n[/poll]\n", - "it should return the right output" - ); - - controller.set("publicPoll", "true"); - assert.strictEqual( - controller.pollOutput, - "[poll type=regular results=always public=true chartType=bar]\n* 1\n* 2\n[/poll]\n", - "it should return the right output" - ); - - controller.set("pollGroups", "test"); - assert.strictEqual( - controller.get("pollOutput"), - "[poll type=regular results=always public=true chartType=bar groups=test]\n* 1\n* 2\n[/poll]\n", - "it should return the right output" - ); - }); - - test("multiple pollOutput", function (assert) { - const controller = setupController(this); - controller.siteSettings.poll_maximum_options = 20; - - controller.setProperties({ - pollType: MULTIPLE_POLL_TYPE, - pollMin: 1, - pollOptions: [{ value: "1" }, { value: "2" }], - }); - assert.strictEqual( - controller.pollOutput, - "[poll type=multiple results=always min=1 max=2 chartType=bar]\n* 1\n* 2\n[/poll]\n", - "it should return the right output" - ); - - controller.set("publicPoll", "true"); - assert.strictEqual( - controller.pollOutput, - "[poll type=multiple results=always min=1 max=2 public=true chartType=bar]\n* 1\n* 2\n[/poll]\n", - "it should return the right output" - ); - }); - - test("staff_only option is not present for non-staff", async function (assert) { - const controller = setupController(this); - controller.currentUser = { staff: false }; - controller.notifyPropertyChange("pollResults"); - - assert.strictEqual( - controller.pollResults.filterBy("value", "staff_only").length, - 0, - "staff_only is not present" - ); - }); - - test("poll result is always by default", function (assert) { - const controller = setupController(this); - assert.strictEqual(controller.pollResult, "always"); - }); - - test("staff_only option is present for staff", async function (assert) { - const controller = setupController(this); - controller.currentUser = { staff: true }; - controller.notifyPropertyChange("pollResults"); - - assert.strictEqual( - controller.pollResults.filterBy("value", "staff_only").length, - 1, - "staff_only is present" - ); - }); -});