mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 09:36:19 -05:00
FEATURE: New "Dropdown" user field type
This commit is contained in:
parent
f22618050f
commit
dc8a68fd29
18 changed files with 248 additions and 52 deletions
|
@ -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);
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<div class='form-element-desc'>
|
||||
<div class='form-element label-area'>
|
||||
{{#if label}}
|
||||
<label>{{i18n label}}</label>
|
||||
{{else}}
|
||||
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='form-element'>
|
||||
<div class='form-element input-area'>
|
||||
{{#if wrapLabel}}
|
||||
<label>{{yield}}</label>
|
||||
{{else}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
2
app/models/user_field_option.rb
Normal file
2
app/models/user_field_option.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class UserFieldOption < ActiveRecord::Base
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
9
db/migrate/20150727193414_create_user_field_options.rb
Normal file
9
db/migrate/20150727193414_create_user_field_options.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
45
test/javascripts/components/combo-box-test.js.es6
Normal file
45
test/javascripts/components/combo-box-test.js.es6
Normal 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');
|
||||
}
|
||||
});
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue