FEATURE: New "Dropdown" user field type

This commit is contained in:
Robin Ward 2015-07-28 12:29:40 -04:00
parent f22618050f
commit dc8a68fd29
18 changed files with 248 additions and 52 deletions

View file

@ -10,6 +10,10 @@ export default Ember.Component.extend(bufferedProperty('userField'), {
return I18n.t('admin.user_fields.description');
}.property(),
bufferedFieldType: function() {
return UserField.fieldTypeById(this.get('buffered.field_type'));
}.property('buffered.field_type'),
_focusOnEdit: function() {
if (this.get('editing')) {
Ember.run.scheduleOnce('afterRender', this, '_focusName');
@ -42,7 +46,14 @@ export default Ember.Component.extend(bufferedProperty('userField'), {
actions: {
save() {
const self = this;
const attrs = this.get('buffered').getProperties('name', 'description', 'field_type', 'editable', 'required', 'show_on_profile');
const buffered = this.get('buffered');
const attrs = buffered.getProperties('name',
'description',
'field_type',
'editable',
'required',
'show_on_profile',
'options');
this.get('userField').save(attrs).then(function(res) {
self.set('userField.id', res.user_field.id);

View file

@ -3,11 +3,19 @@ export default Ember.Component.extend({
_setupCollection: function() {
const values = this.get('values');
this.set('collection', (values && values.length) ? values.split("\n") : []);
if (this.get('inputType') === "array") {
this.set('collection', values || []);
} else {
this.set('collection', (values && values.length) ? values.split("\n") : []);
}
}.on('init').observes('values'),
_collectionChanged: function() {
this.set('values', this.get('collection').join("\n"));
if (this.get('inputType') === "array") {
this.set('values', this.get('collection'));
} else {
this.set('values', this.get('collection').join("\n"));
}
}.observes('collection.@each'),
inputInvalid: Ember.computed.empty('newValue'),

View file

@ -1,8 +1,9 @@
var UserField = Ember.Object.extend({
destroy: function() {
var self = this;
const UserField = Ember.Object.extend({
destroy() {
const self = this;
return new Ember.RSVP.Promise(function(resolve) {
var id = self.get('id');
const id = self.get('id');
if (id) {
return Discourse.ajax("/admin/customize/user_fields/" + id, { type: 'DELETE' }).then(function() {
resolve();
@ -12,8 +13,8 @@ var UserField = Ember.Object.extend({
});
},
save: function(attrs) {
var id = this.get('id');
save(attrs) {
const id = this.get('id');
if (!id) {
return Discourse.ajax("/admin/customize/user_fields", {
type: "POST",
@ -28,8 +29,12 @@ var UserField = Ember.Object.extend({
}
});
const UserFieldType = Ember.Object.extend({
name: Discourse.computed.i18n('id', 'admin.user_fields.field_types.%@')
});
UserField.reopenClass({
findAll: function() {
findAll() {
return Discourse.ajax("/admin/customize/user_fields").then(function(result) {
return result.user_fields.map(function(uf) {
return UserField.create(uf);
@ -37,18 +42,19 @@ UserField.reopenClass({
});
},
fieldTypes: function() {
fieldTypes() {
if (!this._fieldTypes) {
this._fieldTypes = [
Ember.Object.create({id: 'text', name: I18n.t('admin.user_fields.field_types.text') }),
Ember.Object.create({id: 'confirm', name: I18n.t('admin.user_fields.field_types.confirm') })
UserFieldType.create({ id: 'text' }),
UserFieldType.create({ id: 'confirm' }),
UserFieldType.create({ id: 'dropdown', hasOptions: true })
];
}
return this._fieldTypes;
},
fieldTypeById: function(id) {
fieldTypeById(id) {
return this.fieldTypes().findBy('id', id);
}
});

View file

@ -1,11 +1,11 @@
<div class='form-element-desc'>
<div class='form-element label-area'>
{{#if label}}
<label>{{i18n label}}</label>
{{else}}
&nbsp;
{{/if}}
</div>
<div class='form-element'>
<div class='form-element input-area'>
{{#if wrapLabel}}
<label>{{yield}}</label>
{{else}}

View file

@ -11,6 +11,12 @@
{{input value=buffered.description class="user-field-desc"}}
{{/admin-form-row}}
{{#if bufferedFieldType.hasOptions}}
{{#admin-form-row label="admin.user_fields.options"}}
{{value-list values=buffered.options inputType="array"}}
{{/admin-form-row}}
{{/if}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.editable}} {{i18n 'admin.user_fields.editable.title'}}
{{/admin-form-row}}

View file

@ -3,8 +3,9 @@ export default Ember.Component.extend({
attributeBindings: ['tabindex'],
classNames: ['combobox'],
valueAttribute: 'id',
nameProperty: 'name',
buildData(o) {
_buildData(o) {
let result = "";
if (this.resultAttributes) {
this.resultAttributes.forEach(function(a) {
@ -14,19 +15,15 @@ export default Ember.Component.extend({
return result;
},
realNameProperty: function() {
return this.get('nameProperty') || 'name';
}.property('nameProperty'),
render(buffer) {
const nameProperty = this.get('realNameProperty'),
none = this.get('none');
const nameProperty = this.get('nameProperty');
const none = this.get('none');
// Add none option if required
if (typeof none === "string") {
buffer.push('<option value="">' + I18n.t(none) + "</option>");
} else if (typeof none === "object") {
buffer.push("<option value=\"\" " + this.buildData(none) + ">" + Em.get(none, nameProperty) + "</option>");
buffer.push("<option value=\"\" " + this._buildData(none) + ">" + Em.get(none, nameProperty) + "</option>");
}
let selected = this.get('value');
@ -35,18 +32,20 @@ export default Ember.Component.extend({
if (this.get('content')) {
const self = this;
this.get('content').forEach(function(o) {
let val = o[self.get('valueAttribute')];
let val = o[self.get('valueAttribute')] || o;
if (!Em.isNone(val)) { val = val.toString(); }
const selectedText = (val === selected) ? "selected" : "";
buffer.push("<option " + selectedText + " value=\"" + val + "\" " + self.buildData(o) + ">" + Handlebars.Utils.escapeExpression(Em.get(o, nameProperty)) + "</option>");
const name = Ember.get(o, nameProperty) || o;
buffer.push("<option " + selectedText + " value=\"" + val + "\" " + self._buildData(o) + ">" + Handlebars.Utils.escapeExpression(name) + "</option>");
});
}
},
valueChanged: function() {
const $combo = this.$(),
val = this.get('value');
val = this.get('value');
if (val !== undefined && val !== null) {
$combo.select2('val', val.toString());
} else {
@ -54,13 +53,11 @@ export default Ember.Component.extend({
}
}.observes('value'),
contentChanged: function() {
_rerenderOnChange: function() {
this.rerender();
}.observes('content.@each'),
_initializeCombo: function() {
const $elem = this.$(),
self = this;
// Workaround for https://github.com/emberjs/ember.js/issues/9813
// Can be removed when fixed. Without it, the wrong option is selected
@ -70,12 +67,14 @@ export default Ember.Component.extend({
// observer for item names changing (optional)
if (this.get('nameChanges')) {
this.addObserver('content.@each.' + this.get('realNameProperty'), this.rerender);
this.addObserver('content.@each.' + this.get('nameProperty'), this.rerender);
}
const $elem = this.$();
$elem.select2({formatResult: this.comboTemplate, minimumResultsForSearch: 5, width: 'resolve'});
const castInteger = this.get('castInteger');
const self = this;
$elem.on("change", function (e) {
let val = $(e.target).val();
if (val && val.length && castInteger) {

View file

@ -1,6 +1,10 @@
export default Ember.Component.extend({
classNameBindings: [':user-field', 'field.field_type'],
layoutName: function() {
return "components/user-fields/" + this.get('field.field_type');
}.property('field.field_type')
layoutName: Discourse.computed.fmt('field.field_type', 'components/user-fields/%@'),
noneLabel: function() {
if (!this.get('field.required')) {
return 'user_fields.none';
}
}.property('field.required')
});

View file

@ -0,0 +1,6 @@
<label>{{{field.name}}}</label>
<div class='controls'>
{{combo-box content=field.options value=value none=noneLabel}}
{{#if field.required}}<span class='required'>*</span>{{/if}}
<p>{{{field.description}}}</p>
</div>

View file

@ -1437,15 +1437,24 @@ tr.not-activated {
.form-element, .form-element-desc {
float: left;
width: 25%;
height: 30px;
min-height: 30px;
padding: 0.25em 0;
}
.form-element-desc label {
margin: 0.5em 1em 0 0;
text-align: right;
font-weight: bold;
&.input-area {
width: 75%;
input[type=text] {
width: 50%;
}
}
&.label-area {
width: 25%;
label {
margin: 0.5em 1em 0 0;
text-align: right;
font-weight: bold;
}
}
}
.controls {

View file

@ -7,13 +7,16 @@ class Admin::UserFieldsController < Admin::AdminController
def create
field = UserField.new(params.require(:user_field).permit(*Admin::UserFieldsController.columns))
field.required = params[:required] == "true"
fetch_options(field)
json_result(field, serializer: UserFieldSerializer) do
field.save
end
end
def index
render_serialized(UserField.all, UserFieldSerializer, root: 'user_fields')
user_fields = UserField.all.includes(:user_field_options)
render_serialized(user_fields, UserFieldSerializer, root: 'user_fields')
end
def update
@ -24,6 +27,8 @@ class Admin::UserFieldsController < Admin::AdminController
Admin::UserFieldsController.columns.each do |col|
field.send("#{col}=", field_params[col] || false)
end
UserFieldOption.where(user_field_id: field.id).delete_all
fetch_options(field)
json_result(field, serializer: UserFieldSerializer) do
field.save
@ -36,5 +41,14 @@ class Admin::UserFieldsController < Admin::AdminController
render nothing: true
end
protected
def fetch_options(field)
options = params[:user_field][:options]
if options.present?
field.user_field_options_attributes = options.map {|o| {value: o} }.uniq
end
end
end

View file

@ -1,5 +1,7 @@
class UserField < ActiveRecord::Base
validates_presence_of :name, :description, :field_type
has_many :user_field_options, dependent: :destroy
accepts_nested_attributes_for :user_field_options
def self.max_length
2048

View file

@ -0,0 +1,2 @@
class UserFieldOption < ActiveRecord::Base
end

View file

@ -5,5 +5,14 @@ class UserFieldSerializer < ApplicationSerializer
:field_type,
:editable,
:required,
:show_on_profile
:show_on_profile,
:options
def options
object.user_field_options.pluck(:value)
end
def include_options?
options.present?
end
end

View file

@ -388,6 +388,9 @@ en:
post_count: "# posts"
confirm_delete_other_accounts: "Are you sure you want to delete these accounts?"
user_fields:
none: "(select an option)"
user:
said: "{{username}}:"
profile: "Profile"
@ -2331,6 +2334,7 @@ en:
delete: "Delete"
cancel: "Cancel"
delete_confirm: "Are you sure you want to delete that user field?"
options: "Options"
required:
title: "Required at signup?"
enabled: "required"
@ -2347,6 +2351,7 @@ en:
field_types:
text: 'Text Field'
confirm: 'Confirmation'
dropdown: "Dropdown"
site_text:
none: "Choose a type of content to begin editing."

View file

@ -0,0 +1,9 @@
class CreateUserFieldOptions < ActiveRecord::Migration
def change
create_table :user_field_options do |t|
t.references :user_field, null: false
t.string :value, null: false
t.timestamps
end
end
end

View file

@ -16,6 +16,18 @@ describe Admin::UserFieldsController do
expect(response).to be_success
}.to change(UserField, :count).by(1)
end
it "creates a user field with options" do
expect {
xhr :post, :create, {user_field: {name: 'hello',
description: 'hello desc',
field_type: 'dropdown',
options: ['a', 'b', 'c']} }
expect(response).to be_success
}.to change(UserField, :count).by(1)
expect(UserFieldOption.count).to eq(3)
end
end
context '.index' do
@ -50,6 +62,18 @@ describe Admin::UserFieldsController do
expect(user_field.name).to eq('fraggle')
expect(user_field.field_type).to eq('confirm')
end
it "updates the user field options" do
xhr :put, :update, id: user_field.id, user_field: {name: 'fraggle',
field_type: 'dropdown',
description: 'muppet',
options: ['hello', 'hello', 'world']}
expect(response).to be_success
user_field.reload
expect(user_field.name).to eq('fraggle')
expect(user_field.field_type).to eq('dropdown')
expect(user_field.user_field_options.size).to eq(2)
end
end
end

View file

@ -0,0 +1,45 @@
import componentTest from 'helpers/component-test';
moduleForComponent('combo-box', {integration: true});
componentTest('with objects', {
template: '{{combo-box content=items value=value}}',
setup() {
this.set('items', [{id: 1, name: 'hello'}, {id: 2, name: 'world'}]);
},
test(assert) {
assert.equal(this.get('value'), 1);
assert.ok(this.$('.combobox').length);
assert.equal(this.$("select option[value='1']").text(), 'hello');
assert.equal(this.$("select option[value='2']").text(), 'world');
}
});
componentTest('with an array', {
template: '{{combo-box content=items value=value}}',
setup() {
this.set('items', ['evil', 'trout', 'hat']);
},
test(assert) {
assert.equal(this.get('value'), 'evil');
assert.ok(this.$('.combobox').length);
assert.equal(this.$("select option[value='evil']").text(), 'evil');
assert.equal(this.$("select option[value='trout']").text(), 'trout');
}
});
componentTest('with none', {
template: '{{combo-box content=items none="test.none" value=value}}',
setup() {
I18n.translations[I18n.locale].js.test = {none: 'none'};
this.set('items', ['evil', 'trout', 'hat']);
},
test(assert) {
assert.equal(this.$("select option:eq(0)").text(), 'none');
assert.equal(this.$("select option:eq(0)").val(), '');
assert.equal(this.$("select option:eq(1)").text(), 'evil');
assert.equal(this.$("select option:eq(2)").text(), 'trout');
}
});

View file

@ -2,13 +2,11 @@ import componentTest from 'helpers/component-test';
moduleForComponent('value-list', {integration: true});
componentTest('functionality', {
template: '{{value-list value=values}}',
test: function(assert) {
andThen(() => {
assert.ok(this.$('.values .value').length === 0, 'it has no values');
assert.ok(this.$('input').length, 'it renders the input');
assert.ok(this.$('.btn-primary[disabled]').length, 'it is disabled with no value');
});
template: '{{value-list values=values inputType="array"}}',
test(assert) {
assert.ok(this.$('.values .value').length === 0, 'it has no values');
assert.ok(this.$('input').length, 'it renders the input');
assert.ok(this.$('.btn-primary[disabled]').length, 'it is disabled with no value');
fillIn('input', 'eviltrout');
andThen(() => {
@ -17,9 +15,10 @@ componentTest('functionality', {
click('.btn-primary');
andThen(() => {
assert.ok(this.$('.values .value').length === 1, 'it adds the value');
assert.ok(this.$('input').val() === '', 'it clears the input');
assert.equal(this.$('.values .value').length, 1, 'it adds the value');
assert.equal(this.$('input').val(), '', 'it clears the input');
assert.ok(this.$('.btn-primary[disabled]').length, "it is disabled again");
assert.equal(this.get('values'), 'eviltrout', 'it appends the value');
});
click('.value .btn-small');
@ -28,3 +27,41 @@ componentTest('functionality', {
});
}
});
componentTest('with string delimited values', {
template: '{{value-list values=valueString}}',
setup() {
this.set('valueString', "hello\nworld");
},
test(assert) {
assert.equal(this.$('.values .value').length, 2);
fillIn('input', 'eviltrout');
click('.btn-primary');
andThen(() => {
assert.equal(this.$('.values .value').length, 3);
assert.equal(this.get('valueString'), "hello\nworld\neviltrout");
});
}
});
componentTest('with array values', {
template: '{{value-list values=valueArray inputType="array"}}',
setup() {
this.set('valueArray', ['abc', 'def']);
},
test(assert) {
assert.equal(this.$('.values .value').length, 2);
fillIn('input', 'eviltrout');
click('.btn-primary');
andThen(() => {
assert.equal(this.$('.values .value').length, 3);
assert.deepEqual(this.get('valueArray'), ['abc', 'def', 'eviltrout']);
});
}
});