FEATURE: Poll UI Builder.

This commit is contained in:
Guo Xiang Tan 2016-06-13 18:21:14 +08:00
parent 5813352439
commit 8d46727d67
No known key found for this signature in database
GPG key ID: 19C321C8952B0F72
11 changed files with 529 additions and 11 deletions

View file

@ -362,7 +362,7 @@ export default Ember.Component.extend({
this._resetUpload(true);
},
showOptions() {
showOptions(toolbarEvent) {
// long term we want some smart positioning algorithm in popup-menu
// the problem is that positioning in a fixed panel is a nightmare
// cause offsetParent can end up returning a fixed element and then
@ -388,9 +388,8 @@ export default Ember.Component.extend({
left = replyWidth - popupWidth - 40;
}
this.sendAction('showOptions', { position: "absolute",
left: left,
top: top });
this.sendAction('showOptions', toolbarEvent,
{ position: "absolute", left, top });
},
showUploadModal(toolbarEvent) {
@ -420,7 +419,7 @@ export default Ember.Component.extend({
sendAction: 'showUploadModal'
});
if (this.get('canWhisper')) {
if (this.get("popupMenuOptions").some(option => option.condition)) {
toolbar.addButton({
id: 'options',
group: 'extras',

View file

@ -64,7 +64,6 @@ export default Ember.Controller.extend({
init() {
this._super();
const self = this
addPopupMenuOptionsCallback(function() {
return {
@ -114,14 +113,12 @@ export default Ember.Controller.extend({
@computed("model.composeState")
popupMenuOptions(composeState) {
const self = this;
if (composeState === 'open') {
return _popupMenuOptionsCallbacks.map(callback => {
let option = callback();
if (option.condition) {
option.condition = self.get(option.condition);
option.condition = this.get(option.condition);
} else {
option.condition = true;
}
@ -193,7 +190,8 @@ export default Ember.Controller.extend({
this.toggleProperty('showToolbar');
},
showOptions(loc) {
showOptions(toolbarEvent, loc) {
this.set('toolbarEvent', toolbarEvent);
this.appEvents.trigger('popup-menu:open', loc);
this.set('optionsVisible', true);
},

View file

@ -6,7 +6,7 @@
{{#each popupMenuOptions as |option|}}
{{#if option.condition}}
<li>
{{d-button action=option.action icon=option.icon label=option.label}}
{{d-button action=option.action icon=option.icon label=option.label}}
</li>
{{/if}}
{{/each}}
@ -90,6 +90,7 @@
composer=model
lastValidatedAt=lastValidatedAt
canWhisper=canWhisper
popupMenuOptions=popupMenuOptions
draftStatus=model.draftStatus
isUploading=isUploading
groupsMentioned="groupsMentioned"

View file

@ -0,0 +1,158 @@
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
needs: ['modal'],
init() {
this._super();
this._setupPoll();
},
@computed
pollTypes() {
return [I18n.t("poll.ui_builder.poll_type.number"), I18n.t("poll.ui_builder.poll_type.multiple")].map(type => {
return { name: type, value: type };
});
},
@computed("pollType", "pollOptionsCount")
isMultiple(pollType, count) {
return (pollType === I18n.t("poll.ui_builder.poll_type.multiple")) && count > 0;
},
@computed("pollType")
isNumber(pollType) {
return pollType === I18n.t("poll.ui_builder.poll_type.number");
},
@computed("isNumber", "isMultiple")
showMinMax(isNumber, isMultiple) {
return isNumber || isMultiple;
},
@computed("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;
},
@observes("isMultiple", "isNumber", "pollOptionsCount")
_setPollMax() {
const isMultiple = this.get("isMultiple");
const isNumber = this.get("isNumber");
if (!isMultiple && !isNumber) return;
if (isMultiple) {
this.set("pollMax", this.get("pollOptionsCount"));
} else if (isNumber) {
this.set("pollMax", this.siteSettings.poll_maximum_options);
}
},
@computed("isMultiple", "isNumber", "pollOptionsCount")
pollMinOptions(isMultiple, isNumber, count) {
if (!isMultiple && !isNumber) return;
if (isMultiple) {
return this._comboboxOptions(1, count + 1);
} else if (isNumber) {
return this._comboboxOptions(1, this.siteSettings.poll_maximum_options + 1);
}
},
@computed("isMultiple", "isNumber", "pollOptionsCount", "pollMin", "pollStep")
pollMaxOptions(isMultiple, isNumber, count, pollMin, pollStep) {
if (!isMultiple && !isNumber) return;
var range = [];
const pollMinInt = parseInt(pollMin);
if (isMultiple) {
return this._comboboxOptions(pollMinInt + 1, count + 1);
} else if (isNumber) {
const pollStepInt = parseInt(pollStep);
return this._comboboxOptions(pollMinInt + 1, pollMinInt + (this.siteSettings.poll_maximum_options * pollStepInt));
}
},
@computed("isNumber", "pollMax")
pollStepOptions(isNumber, pollMax) {
if (!isNumber) return;
return this._comboboxOptions(1, parseInt(pollMax) + 1);
},
@computed("isNumber", "showMinMax", "pollName", "pollType", "publicPoll", "pollOptions", "pollMin", "pollMax", "pollStep")
pollOutput(isNumber, showMinMax, pollName, pollType, publicPoll, pollOptions, pollMin, pollMax, pollStep) {
let pollHeader = '[poll';
let output = '';
if (pollName) pollHeader += ` name=${pollName.replace(' ', '-')}`;
if (pollType) pollHeader += ` type=${pollType}`;
if (pollMin && showMinMax) pollHeader += ` min=${pollMin}`;
if (pollMax) pollHeader += ` max=${pollMax}`;
if (isNumber) pollHeader += ` step=${pollStep}`;
if (publicPoll) pollHeader += ' public=true';
pollHeader += ']'
output += `${pollHeader}\n`;
if (pollOptions.length > 0 && !isNumber) {
output += `${pollOptions.split("\n").map(option => `* ${option}`).join("\n")}\n`;
}
output += '[/poll]';
return output;
},
@computed("pollOptionsCount", "isNumber")
disableInsert(count, isNumber) {
if (isNumber) {
return false;
} else {
return count < 2;
}
},
@computed("disableInsert")
minNumOfOptionsValidation(disableInsert) {
let options = { ok: true };
if (disableInsert) {
options = { failed: true, reason: I18n.t("poll.ui_builder.help.options_count") };
}
return Discourse.InputValidation.create(options);
},
_comboboxOptions(start_index, end_index) {
return _.range(start_index, end_index).map(number => {
return { value: number, name: number }
})
},
_setupPoll() {
this.setProperties({
pollName: '',
pollNamePlaceholder: I18n.t("poll.ui_builder.poll_name.placeholder"),
pollType: null,
publicPoll: false,
pollOptions: '',
pollMin: 1,
pollMax: null,
pollStep: 1
});
},
actions: {
insertPoll() {
this.get("toolbarEvent").addText(this.get("pollOutput"));
this.send("closeModal");
}
}
});

View file

@ -0,0 +1,57 @@
<div class="modal-body poll-ui-builder">
<form class="poll-ui-builder-form form-horizontal">
<div class="input-group">
<label>{{i18n 'poll.ui_builder.poll_name.label'}}</label>
{{input name="poll-name" value=pollName placeholder=pollNamePlaceholder}}
</div>
<div class="input-group">
<label>{{i18n 'poll.ui_builder.poll_type.label'}}</label>
{{combo-box content=pollTypes
value=pollType
valueAttribute="value"
none="poll.ui_builder.poll_type.regular"}}
{{#if showMinMax}}
<label>{{i18n 'poll.ui_builder.poll_config.min'}}</label>
{{combo-box content=pollMinOptions
value=pollMin
valueAttribute="value"
class="poll-options-min"}}
<label>{{i18n 'poll.ui_builder.poll_config.max'}}</label>
{{combo-box content=pollMaxOptions
value=pollMax
valueAttribute="value"
class="poll-options-max"}}
{{#if isNumber}}
<label>{{i18n 'poll.ui_builder.poll_config.step'}}</label>
{{combo-box content=pollStepOptions
value=pollStep
valueAttribute="value"
class="poll-options-step"}}
{{/if}}
{{/if}}
</div>
<div class="input-group">
<label for="poll-public">
{{input type='checkbox' checked=publicPoll}}
{{i18n "poll.ui_builder.poll_public.label"}}
</label>
</div>
{{#unless isNumber}}
<div class="input-group">
<label>{{i18n 'poll.ui_builder.poll_options.label'}}</label>
{{input-tip validation=minNumOfOptionsValidation}}
{{d-editor value=pollOptions}}
</div>
{{/unless}}
</form>
</div>
<div class="modal-footer">
{{d-button action="insertPoll" class='btn-primary' label='poll.ui_builder.insert' disabled=disableInsert}}
</div>

View file

@ -0,0 +1,30 @@
import { withPluginApi } from 'discourse/lib/plugin-api';
import showModal from 'discourse/lib/show-modal';
function initializePollUIBuilder(api) {
const ComposerController = api.container.lookup("controller:composer");
ComposerController.reopen({
actions: {
showPollBuilder() {
showModal("poll-ui-builder").set("toolbarEvent", this.get("toolbarEvent"));
}
}
});
api.addToolbarPopupMenuOptionsCallback(function() {
return {
action: 'showPollBuilder',
icon: 'bar-chart-o',
label: 'poll.ui_builder.title'
};
});
}
export default {
name: "add-poll-ui-builder",
initialize() {
withPluginApi('0.1', initializePollUIBuilder);
}
};

View file

@ -0,0 +1,8 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
needs: ['modal'],
templateName: 'modals/poll-ui-builder',
title: I18n.t("poll.ui_builder.title")
});

View file

@ -0,0 +1,18 @@
.poll-ui-builder-form {
.input-group {
padding: 10px;
}
label {
font-weight: bold;
display: inline;
}
.combobox {
margin-right: 5px;
}
.poll-options-min, .poll-options-max, .poll-options-step {
width: 70px !important;
}
}

View file

@ -68,3 +68,26 @@ en:
error_while_toggling_status: "There was an error while toggling the status of this poll."
error_while_casting_votes: "There was an error while casting your votes."
error_while_fetching_voters: "There was an error while displaying the voters."
ui_builder:
title: Poll Builder
insert: Insert Poll
reset: Reset Poll
help:
options_count: You must provide a minimum of 2 options.
poll_name:
label: Poll Name
placeholder: Enter Poll Name
poll_type:
label: Poll Type
regular: regular
multiple: multiple
number: number
poll_config:
max: Max
min: Min
step: Step
poll_public:
label: Make Public Poll
poll_options:
label: "Poll Choices: (one option per line)"

View file

@ -7,6 +7,7 @@
enabled_site_setting :poll_enabled
register_asset "stylesheets/common/poll.scss"
register_asset "stylesheets/common/poll-ui-builder.scss"
register_asset "stylesheets/desktop/poll.scss", :desktop
register_asset "stylesheets/mobile/poll.scss", :mobile

View file

@ -0,0 +1,225 @@
moduleFor("controller:poll-ui-builder", "controller:poll-ui-builder", {
needs: ['controller:modal']
});
test("isMultiple", function() {
const controller = this.subject();
controller.setProperties({
pollType: I18n.t("poll.ui_builder.poll_type.multiple"),
pollOptionsCount: 1
});
equal(controller.get("isMultiple"), true, "it should be true");
controller.set("pollOptionsCount", 0);
equal(controller.get("isMultiple"), false, "it should be false");
controller.setProperties({ pollType: "random", pollOptionsCount: 1 });
equal(controller.get("isMultiple"), false, "it should be false");
});
test("isNumber", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.set("pollType", "random");
equal(controller.get("isNumber"), false, "it should be false");
controller.set("pollType", I18n.t("poll.ui_builder.poll_type.number"));
equal(controller.get("isNumber"), true, "it should be true");
});
test("showMinMax", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.setProperties({
isNumber: true,
isMultiple: false
});
equal(controller.get("showMinMax"), true, "it should be true");
controller.setProperties({
isNumber: false,
isMultiple: true
});
equal(controller.get("showMinMax"), true, "it should be true");
});
test("pollOptionsCount", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.set("pollOptions", "1\n2\n")
equal(controller.get("pollOptionsCount"), 2, "it should equal 2");
controller.set("pollOptions", "")
equal(controller.get("pollOptionsCount"), 0, "it should equal 0");
});
test("pollMinOptions", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.setProperties({
isMultiple: true,
pollOptionsCount: 1
});
deepEqual(controller.get("pollMinOptions"), [{ name: 1, value: 1 }], "it should return the right options");
controller.set("pollOptionsCount", 2);
deepEqual(controller.get("pollMinOptions"), [
{ name: 1, value: 1 }, { name: 2, value: 2 }
], "it should return the right options");
controller.set("isNumber", true);
controller.siteSettings.poll_maximum_options = 2;
deepEqual(controller.get("pollMinOptions"), [
{ name: 1, value: 1 }, { name: 2, value: 2 }
], "it should return the right options");
});
test("pollMaxOptions", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.setProperties({ isMultiple: true, pollOptionsCount: 1, pollMin: 1 });
deepEqual(controller.get("pollMaxOptions"), [], "it should return the right options");
controller.set("pollOptionsCount", 2);
deepEqual(controller.get("pollMaxOptions"), [
{ name: 2, value: 2 }
], "it should return the right options");
controller.siteSettings.poll_maximum_options = 3;
controller.setProperties({ isMultiple: false, isNumber: true, pollStep: 2, pollMin: 1 });
deepEqual(controller.get("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() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.siteSettings.poll_maximum_options = 3;
controller.set("isNumber", false);
equal(controller.get("pollStepOptions"), null, "is should return null");
controller.setProperties({ isNumber: true });
deepEqual(controller.get("pollStepOptions"), [
{ name: 1, value: 1 },
{ name: 2, value: 2 },
{ name: 3, value: 3 }
], "it should return the right options");
});
test("disableInsert", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.setProperties({ isNumber: true });
equal(controller.get("disableInsert"), false, "it should be false");
controller.setProperties({ isNumber: false, pollOptionsCount: 3 });
equal(controller.get("disableInsert"), false, "it should be false");
controller.setProperties({ isNumber: false, pollOptionsCount: 1 });
equal(controller.get("disableInsert"), true, "it should be true");
});
test("number pollOutput", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.siteSettings.poll_maximum_options = 20;
controller.setProperties({
isNumber: true,
pollType: I18n.t("poll.ui_builder.poll_type.number"),
pollMin: 1
});
equal(controller.get("pollOutput"), "[poll type=number min=1 max=20 step=1]\n[/poll]", "it should return the right output");
controller.set("pollName", 'test');
equal(controller.get("pollOutput"), "[poll name=test type=number min=1 max=20 step=1]\n[/poll]", "it should return the right output");
controller.set("pollName", 'test poll');
equal(controller.get("pollOutput"), "[poll name=test-poll type=number min=1 max=20 step=1]\n[/poll]", "it should return the right output");
controller.set("pollStep", 2);
equal(controller.get("pollOutput"), "[poll name=test-poll type=number min=1 max=20 step=2]\n[/poll]", "it should return the right output");
controller.set("publicPoll", true);
equal(controller.get("pollOutput"), "[poll name=test-poll type=number min=1 max=20 step=2 public=true]\n[/poll]", "it should return the right output");
});
test("regular pollOutput", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.siteSettings.poll_maximum_options = 20;
controller.set("pollOptions", "1\n2");
equal(controller.get("pollOutput"), "[poll]\n* 1\n* 2\n[/poll]", "it should return the right output");
controller.set("pollName", "test");
equal(controller.get("pollOutput"), "[poll name=test]\n* 1\n* 2\n[/poll]", "it should return the right output");
controller.set("publicPoll", "true");
equal(controller.get("pollOutput"), "[poll name=test public=true]\n* 1\n* 2\n[/poll]", "it should return the right output");
});
test("multiple pollOutput", function() {
const controller = this.subject();
controller.siteSettings = Discourse.SiteSettings;
controller.siteSettings.poll_maximum_options = 20;
controller.setProperties({
isMultiple: true,
pollType: I18n.t("poll.ui_builder.poll_type.multiple"),
pollMin: 1,
pollOptions: "1\n2"
});
equal(controller.get("pollOutput"), "[poll type=multiple min=1 max=2]\n* 1\n* 2\n[/poll]", "it should return the right output");
controller.set("pollName", "test");
equal(controller.get("pollOutput"), "[poll name=test type=multiple min=1 max=2]\n* 1\n* 2\n[/poll]", "it should return the right output");
controller.set("publicPoll", "true");
equal(controller.get("pollOutput"), "[poll name=test type=multiple min=1 max=2 public=true]\n* 1\n* 2\n[/poll]", "it should return the right output");
});