FEATURE: Admin interface for adding custom fields for users
This commit is contained in:
parent
a3e2e1fa6e
commit
0fc0533134
|
@ -0,0 +1,43 @@
|
|||
import UserField from 'admin/models/user-field';
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
|
||||
export default Ember.ObjectController.extend(BufferedContent, {
|
||||
needs: ['admin-user-fields'],
|
||||
editing: Ember.computed.empty('id'),
|
||||
|
||||
fieldName: function() {
|
||||
return UserField.fieldTypeById(this.get('field_type')).get('name');
|
||||
}.property('field_type'),
|
||||
|
||||
actions: {
|
||||
save: function() {
|
||||
var self = this;
|
||||
|
||||
this.commitBuffer();
|
||||
this.get('model').save().then(function(res) {
|
||||
self.set('model.id', res.user_field.id);
|
||||
self.set('editing', false);
|
||||
}).catch(function() {
|
||||
bootbox.alert(I18n.t('generic_error'));
|
||||
});
|
||||
},
|
||||
|
||||
edit: function() {
|
||||
this.set('editing', true);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.get('controllers.admin-user-fields').send('destroy', this.get('model'));
|
||||
},
|
||||
|
||||
cancel: function() {
|
||||
var id = this.get('id');
|
||||
if (Ember.empty(id)) {
|
||||
this.get('controllers.admin-user-fields').send('destroy', this.get('model'));
|
||||
} else {
|
||||
this.rollbackBuffer();
|
||||
this.set('editing', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import UserField from 'admin/models/user-field';
|
||||
|
||||
export default Ember.ArrayController.extend({
|
||||
fieldTypes: null,
|
||||
|
||||
createDisabled: Em.computed.gte('model.length', 3),
|
||||
|
||||
_performDestroy: function(f, model) {
|
||||
return f.destroy().then(function() {
|
||||
model.removeObject(f);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
createField: function() {
|
||||
this.pushObject(UserField.create({
|
||||
field_type: 'text',
|
||||
name: I18n.t('admin.user_fields.untitled')
|
||||
}));
|
||||
},
|
||||
|
||||
destroy: function(f) {
|
||||
var model = this.get('model'),
|
||||
self = this;
|
||||
|
||||
// Only confirm if we already been saved
|
||||
if (f.get('id')) {
|
||||
bootbox.confirm(I18n.t("admin.user_fields.delete_confirm"), function(result) {
|
||||
if (result) { self._performDestroy(f, model); }
|
||||
});
|
||||
} else {
|
||||
self._performDestroy(f, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
var _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') })
|
||||
];
|
||||
|
||||
var UserField = Ember.Object.extend({
|
||||
destroy: function() {
|
||||
var self = this;
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
var id = self.get('id');
|
||||
if (id) {
|
||||
return Discourse.ajax("/admin/customize/user_fields/" + id, { type: 'DELETE' }).then(function() {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
||||
save: function() {
|
||||
var id = this.get('id');
|
||||
if (!id) {
|
||||
return Discourse.ajax("/admin/customize/user_fields", {
|
||||
type: "POST",
|
||||
data: { user_field: this.getProperties('name', 'field_type') }
|
||||
});
|
||||
} else {
|
||||
return Discourse.ajax("/admin/customize/user_fields/" + id, {
|
||||
type: "PUT",
|
||||
data: { user_field: this.getProperties('name', 'field_type') }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
UserField.reopenClass({
|
||||
findAll: function() {
|
||||
return Discourse.ajax("/admin/customize/user_fields").then(function(result) {
|
||||
return result.user_fields.map(function(uf) {
|
||||
return UserField.create(uf);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
fieldTypes: function() {
|
||||
return _fieldTypes;
|
||||
},
|
||||
|
||||
fieldTypeById: function(id) {
|
||||
return _fieldTypes.findBy('id', id);
|
||||
}
|
||||
});
|
||||
|
||||
export default UserField;
|
|
@ -0,0 +1,14 @@
|
|||
import UserField from 'admin/models/user-field';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model: function() {
|
||||
return UserField.findAll();
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties({
|
||||
model: model,
|
||||
fieldTypes: UserField.fieldTypes()
|
||||
});
|
||||
}
|
||||
});
|
|
@ -18,6 +18,8 @@ Discourse.Route.buildRoutes(function() {
|
|||
this.resource('adminSiteText', { path: '/site_text' }, function() {
|
||||
this.route('edit', {path: '/:text_type'});
|
||||
});
|
||||
this.resource('adminUserFields', { path: '/user_fields' }, function() {
|
||||
});
|
||||
});
|
||||
this.route('api');
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<li>{{#link-to 'adminCustomize.colors'}}{{i18n admin.customize.colors.title}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomize.css_html'}}{{i18n admin.customize.css_html.title}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminSiteText'}}{{i18n admin.site_text.title}}{{/link-to}}</li>
|
||||
{{#if USERFIELD_FEATURE_COMPLETE}}
|
||||
<li>{{#link-to 'adminUserFields'}}{{i18n admin.user_fields.title}}{{/link-to}}</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<div class='user-fields'>
|
||||
<h2>{{i18n admin.user_fields.title}}</h2>
|
||||
|
||||
<p class="desc">{{i18n admin.user_fields.description}}</p>
|
||||
|
||||
{{#if model}}
|
||||
{{#each f in model itemController="admin-user-field-item" itemView="admin-user-field-item"}}
|
||||
{{#if f.editing}}
|
||||
<div class='form-element'>
|
||||
<label>{{i18n admin.user_fields.name}}
|
||||
{{input value=f.buffered.name class="user-field-name"}}
|
||||
</label>
|
||||
</div>
|
||||
<div class='form-element'>
|
||||
<label>{{i18n admin.user_fields.type}}
|
||||
{{combo-box content=fieldTypes valueAttribute="id" value=f.buffered.field_type}}
|
||||
</label>
|
||||
</div>
|
||||
<div class='form-element controls'>
|
||||
<button {{action "save"}}class='btn btn-primary'>{{fa-icon 'check'}} {{i18n admin.user_fields.save}}</button>
|
||||
<button {{action "cancel"}} class='btn btn-danger'>{{fa-icon 'times'}} {{i18n admin.user_fields.cancel}}</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class='form-display'>
|
||||
{{f.name}}
|
||||
</div>
|
||||
<div class='form-display'>
|
||||
{{f.fieldName}}
|
||||
</div>
|
||||
<div class='form-element controls'>
|
||||
<button {{action "edit"}}class='btn btn-default'>{{fa-icon 'pencil'}} {{i18n admin.user_fields.edit}}</button>
|
||||
<button {{action "destroy"}}class='btn btn-danger'>{{fa-icon 'trash-o'}} {{i18n admin.user_fields.delete}}</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class='clearfix'></div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
<button {{bind-attr disabled="createDisabled"}} class='btn btn-primary' {{action createField}}>
|
||||
{{fa-icon "plus"}}
|
||||
{{i18n admin.user_fields.create}}
|
||||
</button>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
export default Ember.View.extend({
|
||||
classNameBindings: [':user-field'],
|
||||
|
||||
_focusOnEdit: function() {
|
||||
if (this.get('controller.editing')) {
|
||||
Ember.run.scheduleOnce('afterRender', this, '_focusName');
|
||||
}
|
||||
}.observes('controller.editing').on('didInsertElement'),
|
||||
|
||||
_focusName: function() {
|
||||
$('.user-field-name').select();
|
||||
}
|
||||
});
|
|
@ -136,7 +136,8 @@ export default Ember.DefaultResolver.extend({
|
|||
decamelized = decamelized.replace(/^admin\_/, 'admin/templates/');
|
||||
decamelized = decamelized.replace(/^admin\./, 'admin/templates/');
|
||||
decamelized = decamelized.replace(/\./, '_');
|
||||
return Ember.TEMPLATES[decamelized];
|
||||
var dashed = decamelized.replace(/_/g, '-');
|
||||
return Ember.TEMPLATES[decamelized] || Ember.TEMPLATES[dashed];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/* global BufferedProxy: true */
|
||||
export default Ember.Mixin.create({
|
||||
buffered: function() {
|
||||
return Em.ObjectProxy.extend(BufferedProxy).create({
|
||||
content: this.get('content')
|
||||
});
|
||||
}.property('content'),
|
||||
|
||||
rollbackBuffer: function() {
|
||||
this.get('buffered').discardBufferedChanges();
|
||||
},
|
||||
|
||||
commitBuffer: function() {
|
||||
this.get('buffered').applyBufferedChanges();
|
||||
}
|
||||
});
|
|
@ -1,3 +1,4 @@
|
|||
//= require admin/models/user-field
|
||||
//= require admin/controllers/admin-email-skipped
|
||||
//= require admin/controllers/change-site-customization-details
|
||||
//= require_tree ./admin
|
||||
|
|
|
@ -39,4 +39,5 @@
|
|||
//= require lock-on.js
|
||||
//= require ember-cloaking
|
||||
//= require break_string
|
||||
//= require buffered-proxy
|
||||
//= require_tree ./discourse/ember
|
||||
|
|
|
@ -1319,3 +1319,43 @@ tr.not-activated {
|
|||
color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.user-fields {
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-field {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid scale-color-diff();
|
||||
|
||||
.form-display {
|
||||
width: 35%;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.form-element {
|
||||
float: left;
|
||||
width: 35%;
|
||||
margin-right: 10px;
|
||||
label {
|
||||
margin-right: 10px;
|
||||
}
|
||||
input, div.combobox {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
float: right;
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
class Admin::UserFieldsController < Admin::AdminController
|
||||
|
||||
def create
|
||||
field = UserField.create!(params.require(:user_field).permit(:name, :field_type))
|
||||
render_serialized(field, UserFieldSerializer)
|
||||
end
|
||||
|
||||
def index
|
||||
render_serialized(UserField.all, UserFieldSerializer, root: 'user_fields')
|
||||
end
|
||||
|
||||
def update
|
||||
field_params = params.require(:user_field)
|
||||
|
||||
field = UserField.where(id: params.require(:id)).first
|
||||
field.name = field_params[:name]
|
||||
field.field_type = field_params[:field_type]
|
||||
field.save
|
||||
|
||||
render_serialized(field, UserFieldSerializer)
|
||||
end
|
||||
|
||||
def destroy
|
||||
field = UserField.where(id: params.require(:id)).first
|
||||
field.destroy if field.present?
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
class UserField < ActiveRecord::Base
|
||||
validates_presence_of :name, :field_type
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
class UserFieldSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :field_type
|
||||
end
|
|
@ -1983,6 +1983,23 @@ en:
|
|||
external_email: "Email"
|
||||
external_avatar_url: "Avatar URL"
|
||||
|
||||
user_fields:
|
||||
title: "User Fields"
|
||||
description: "Any fields added here will be required from users when they sign up."
|
||||
create: "Create User Field"
|
||||
untitled: "Untitled"
|
||||
name: "Field Name"
|
||||
type: "Field Type"
|
||||
save: "Save"
|
||||
edit: "Edit"
|
||||
delete: "Delete"
|
||||
cancel: "Cancel"
|
||||
delete_confirm: "Are you sure you want to delete that user field?"
|
||||
|
||||
field_types:
|
||||
text: 'Text Field'
|
||||
confirm: 'Confirmation'
|
||||
|
||||
site_text:
|
||||
none: "Choose a type of content to begin editing."
|
||||
title: 'Text Content'
|
||||
|
|
|
@ -112,6 +112,7 @@ Discourse::Application.routes.draw do
|
|||
scope "/customize" do
|
||||
resources :site_text, constraints: AdminConstraint.new
|
||||
resources :site_text_types, constraints: AdminConstraint.new
|
||||
resources :user_fields, constraints: AdminConstraint.new
|
||||
end
|
||||
|
||||
resources :color_schemes, constraints: AdminConstraint.new
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class CreateUserFields < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :user_fields do |t|
|
||||
t.string :name, null: false
|
||||
t.string :field_type, null: false
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -70,7 +70,7 @@ module Tilt
|
|||
# For backwards compatibility with plugins, for now export the Global format too.
|
||||
# We should eventually have an upgrade system for plugins to use ES6 or some other
|
||||
# resolve based API.
|
||||
if ENV['DISCOURSE_NO_CONSTANTS'].nil? && scope.logical_path =~ /(discourse|admin)\/(controllers|components|views|routes|mixins)\/(.*)/
|
||||
if ENV['DISCOURSE_NO_CONSTANTS'].nil? && scope.logical_path =~ /(discourse|admin)\/(controllers|components|views|routes|mixins|models)\/(.*)/
|
||||
type = Regexp.last_match[2]
|
||||
file_name = Regexp.last_match[3].gsub(/[\-\/]/, '_')
|
||||
class_name = file_name.classify
|
||||
|
@ -87,6 +87,7 @@ module Tilt
|
|||
# HAX
|
||||
result = "Controller" if result == "ControllerController"
|
||||
result.gsub!(/Mixin$/, '')
|
||||
result.gsub!(/Model$/, '')
|
||||
|
||||
@output << "\n\nDiscourse.#{result} = require('#{require_name}').default;\n"
|
||||
end
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Admin::UserFieldsController do
|
||||
|
||||
it "is a subclass of AdminController" do
|
||||
(Admin::ApiController < Admin::AdminController).should == true
|
||||
end
|
||||
|
||||
context "when logged in" do
|
||||
let!(:user) { log_in(:admin) }
|
||||
|
||||
context '.create' do
|
||||
it "creates a user field" do
|
||||
-> {
|
||||
xhr :post, :create, {user_field: {name: 'hello', field_type: 'text'} }
|
||||
response.should be_success
|
||||
}.should change(UserField, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context '.index' do
|
||||
let!(:user_field) { Fabricate(:user_field) }
|
||||
|
||||
it "returns a list of user fields" do
|
||||
xhr :get, :index
|
||||
response.should be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
json['user_fields'].should be_present
|
||||
end
|
||||
end
|
||||
|
||||
context '.destroy' do
|
||||
let!(:user_field) { Fabricate(:user_field) }
|
||||
|
||||
it "returns a list of user fields" do
|
||||
-> {
|
||||
xhr :delete, :destroy, id: user_field.id
|
||||
response.should be_success
|
||||
}.should change(UserField, :count).by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
context '.update' do
|
||||
let!(:user_field) { Fabricate(:user_field) }
|
||||
|
||||
it "returns a list of user fields" do
|
||||
xhr :put, :update, id: user_field.id, user_field: {name: 'fraggle', field_type: 'confirm'}
|
||||
response.should be_success
|
||||
user_field.reload
|
||||
user_field.name.should == 'fraggle'
|
||||
user_field.field_type.should == 'confirm'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Fabricator(:user_field) do
|
||||
name { sequence(:name) {|i| "field_#{i}" } }
|
||||
field_type 'text'
|
||||
end
|
|
@ -0,0 +1,87 @@
|
|||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
function empty(obj) {
|
||||
var key;
|
||||
for (key in obj) if (obj.hasOwnProperty(key)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var Ember = global.Ember,
|
||||
get = Ember.get, set = Ember.set;
|
||||
|
||||
var BufferedProxy = Ember.Mixin.create({
|
||||
buffer: null,
|
||||
|
||||
hasBufferedChanges: false,
|
||||
|
||||
unknownProperty: function (key) {
|
||||
var buffer = this.buffer;
|
||||
return buffer && buffer.hasOwnProperty(key) ? buffer[key] : this._super(key);
|
||||
},
|
||||
|
||||
setUnknownProperty: function (key, value) {
|
||||
if (!this.buffer) this.buffer = {};
|
||||
|
||||
var buffer = this.buffer,
|
||||
content = this.get('content'),
|
||||
current = content && get(content, key),
|
||||
previous = buffer.hasOwnProperty(key) ? buffer[key] : current;
|
||||
|
||||
if (previous === value) return;
|
||||
|
||||
this.propertyWillChange(key);
|
||||
|
||||
if (current === value) {
|
||||
delete buffer[key];
|
||||
if (empty(buffer)) {
|
||||
this.set('hasBufferedChanges', false);
|
||||
}
|
||||
} else {
|
||||
buffer[key] = value;
|
||||
this.set('hasBufferedChanges', true);
|
||||
}
|
||||
|
||||
this.propertyDidChange(key);
|
||||
return value;
|
||||
},
|
||||
|
||||
applyBufferedChanges: function() {
|
||||
var buffer = this.buffer,
|
||||
content = this.get('content'),
|
||||
key;
|
||||
for (key in buffer) {
|
||||
if (!buffer.hasOwnProperty(key)) continue;
|
||||
set(content, key, buffer[key]);
|
||||
}
|
||||
this.buffer = {};
|
||||
this.set('hasBufferedChanges', false);
|
||||
},
|
||||
|
||||
discardBufferedChanges: function() {
|
||||
var buffer = this.buffer,
|
||||
content = this.get('content'),
|
||||
key;
|
||||
for (key in buffer) {
|
||||
if (!buffer.hasOwnProperty(key)) continue;
|
||||
|
||||
this.propertyWillChange(key);
|
||||
delete buffer[key];
|
||||
this.propertyDidChange(key);
|
||||
}
|
||||
this.set('hasBufferedChanges', false);
|
||||
}
|
||||
});
|
||||
|
||||
// CommonJS module
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = BufferedProxy;
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
define("buffered-proxy", function (require, exports, module) {
|
||||
return BufferedProxy;
|
||||
});
|
||||
} else {
|
||||
global.BufferedProxy = BufferedProxy;
|
||||
}
|
||||
|
||||
}(this));
|
Loading…
Reference in New Issue