diff --git a/app/assets/javascripts/admin/components/list_setting_component.js b/app/assets/javascripts/admin/components/list_setting_component.js new file mode 100644 index 00000000000..9f39d46ce6f --- /dev/null +++ b/app/assets/javascripts/admin/components/list_setting_component.js @@ -0,0 +1,82 @@ +/** + Provide a nice GUI for a pipe-delimited list in the site settings. + + @param settingValue is a reference to SiteSetting.value. + + @class Discourse.ListSettingComponent + @extends Ember.Component + @namespace Discourse + @module Discourse + **/ +Discourse.ListSettingComponent = Ember.Component.extend({ + layoutName: 'components/list-setting', + + init: function() { + this._super(); + this.on("focusOut", this.uncacheValue); + this.set('children', []); + }, + + canAddNew: true, + + readValues: function() { + return this.get('settingValue').split('|'); + }.property('settingValue'), + + /** + Transfer the debounced value into the settingValue parameter. + + This will cause a redraw of the child textboxes. + + @param newFocus {Number|undefined} Which list index to focus on next, or undefined to not refocus + **/ + uncacheValue: function(newFocus) { + var oldValue = this.get('settingValue'), + newValue = this.get('settingValueCached'), + self = this; + + if (newValue !== undefined && newValue !== oldValue) { + this.set('settingValue', newValue); + } + + if (newFocus !== undefined && newFocus > 0) { + Em.run.schedule('afterRender', function() { + var children = self.get('children'); + if (newFocus < children.length) { + $(children[newFocus].get('element')).focus(); + } else if (newFocus === children.length) { + $(self.get('element')).children().children('.list-add-value').focus(); + } + }); + } + }, + + setItemValue: function(index, item) { + var values = this.get('readValues'); + values[index] = item; + + // Remove blank items + values = values.filter(function(s) { return s !== ''; }); + this.setProperties({ + settingValueCached: values.join('|'), + canAddNew: true + }); + }, + + actions: { + addNewItem: function() { + var newValue = this.get('settingValue') + '|'; + this.setProperties({ + settingValue: newValue, + settingValueCached: newValue, + canAddNew: false + }); + + var self = this; + Em.run.schedule('afterRender', function() { + var children = self.get('children'); + $(children[children.length - 1].get('element')).focus(); + }); + } + } +}); diff --git a/app/assets/javascripts/admin/components/list_setting_item_component.js b/app/assets/javascripts/admin/components/list_setting_item_component.js new file mode 100644 index 00000000000..fac5f339b87 --- /dev/null +++ b/app/assets/javascripts/admin/components/list_setting_item_component.js @@ -0,0 +1,44 @@ +/** + One item in a ListSetting. + + @param parent is the ListSettingComponent. + + @class Discourse.ListSettingItemComponent + @extends Ember.Component, Ember.TextSupport + @namespace Discourse + @module Discourse + **/ +Discourse.ListSettingItemComponent = Ember.Component.extend(Ember.TextSupport, { + classNames: ['ember-text-field'], + tagName: "input", + attributeBindings: ['type', 'value', 'size', 'pattern'], + + _initialize: function() { + // _parentView is the #each + // parent is the ListSettingComponent + this.setProperties({ + value: this.get('_parentView.content'), + index: this.get('_parentView.contentIndex') + }); + this.get('parent').get('children')[this.get('index')] = this; + }.on('init'), + + markTab: function(e) { + var keyCode = e.keyCode || e.which; + + if (keyCode === 9) { + this.set('nextIndex', this.get('index') + (e.shiftKey ? -1 : 1)); + } + }.on('keyDown'), + + reloadList: function() { + var nextIndex = this.get('nextIndex'); + this.set('nextIndex', undefined); // one use only + this.get('parent').uncacheValue(nextIndex); + }.on('focusOut'), + + _elementValueDidChange: function() { + this._super(); + this.get('parent').setItemValue(this.get('index'), this.get('value')); + } +}); diff --git a/app/assets/javascripts/admin/templates/site_settings/setting_list.js.handlebars b/app/assets/javascripts/admin/templates/site_settings/setting_list.js.handlebars new file mode 100644 index 00000000000..d7423ea8107 --- /dev/null +++ b/app/assets/javascripts/admin/templates/site_settings/setting_list.js.handlebars @@ -0,0 +1,17 @@ +
+

{{unbound setting}}

+
+
+ {{list-setting settingValue=value}} +
{{unbound description}}
+
+{{#if dirty}} +
+ + +
+{{else}} + {{#if overridden}} + + {{/if}} +{{/if}} diff --git a/app/assets/javascripts/admin/views/site_setting_view.js b/app/assets/javascripts/admin/views/site_setting_view.js index 5e5c05dca75..b288d36c96d 100644 --- a/app/assets/javascripts/admin/views/site_setting_view.js +++ b/app/assets/javascripts/admin/views/site_setting_view.js @@ -10,13 +10,15 @@ Discourse.SiteSettingView = Discourse.View.extend(Discourse.ScrollTop, { classNameBindings: [':row', ':setting', 'content.overridden'], templateName: function() { - - // If we're editing a boolean, return a different template + // If we're editing a boolean, show a checkbox if (this.get('content.type') === 'bool') return 'admin/templates/site_settings/setting_bool'; // If we're editing an enum field, show a dropdown if (this.get('content.type') === 'enum' ) return 'admin/templates/site_settings/setting_enum'; + // If we're editing a list, show a list editor + if (this.get('content.type') === 'list' ) return 'admin/templates/site_settings/setting_list'; + // Default to string editor return 'admin/templates/site_settings/setting_string'; diff --git a/app/assets/javascripts/discourse/templates/components/list-setting.js.handlebars b/app/assets/javascripts/discourse/templates/components/list-setting.js.handlebars new file mode 100644 index 00000000000..ddf8dcabe39 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/list-setting.js.handlebars @@ -0,0 +1,8 @@ +
+ {{#each readValues}} + {{list-setting-item parent=view action=update classNames="list-input-item"}} + {{/each}} + {{#if canAddNew}} + + {{/if}} +
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index be08fea4f66..8010dcf0a42 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -194,6 +194,40 @@ @include medium-width { width: 314px; } @include small-width { width: 284px; } } + .input-setting-list { + width: 408px; + padding: 1px; + background-color: white; + border: 1px solid #e6e6e6; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; + + .list-input-item { + width: 90px; + margin: 2px 1px; + background-color: white; + border: 1px solid #e6e6e6; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; + + &:focus { + border-color: #00aaff; + outline: 0; + box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3), 0 0 8px #00aaff; + } + } + + .btn.list-add-value { + margin: 0px 3px; + padding: 4px 10px; + color: $link-color; + } + } + .desc { padding-top: 3px; diff --git a/config/site_settings.yml b/config/site_settings.yml index 44d96719b4c..124128a0e26 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -53,15 +53,19 @@ basic: top_menu: client: true refresh: true + list: true default: 'latest|new|unread|starred|top|categories' post_menu: client: true + list: true default: 'like|edit|flag|delete|share|bookmark|reply' share_links: client: true + list: true default: 'twitter|facebook|google+|email' category_colors: client: true + list: true default: 'BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890' enable_mobile_theme: client: true @@ -276,6 +280,7 @@ files: client: true default: '.jpg|.jpeg|.png|.gif' refresh: true + list: true crawl_images: default: test: false @@ -340,9 +345,15 @@ security: spam: add_rel_nofollow_to_user_content: true - exclude_rel_nofollow_domains: '' - email_domains_blacklist: 'mailinator.com' - email_domains_whitelist: '' + exclude_rel_nofollow_domains: + default: '' + list: true + email_domains_blacklist: + default: 'mailinator.com' + list: true + email_domains_whitelist: + default: '' + list: true flags_required_to_hide_post: 3 cooldown_minutes_after_hiding_posts: 10 num_flags_to_block_new_user: 3 @@ -350,7 +361,9 @@ spam: notify_mods_when_user_blocked: false flag_sockpuppets: true newuser_spam_host_threshold: 3 - white_listed_spam_host_domains: "" + white_listed_spam_host_domains: + default: '' + list: true rate_limits: unique_posts_mins: diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 8c19f3a1271..1975c5513d9 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -14,7 +14,7 @@ module SiteSettingExtension end def types - @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum) + @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum, :list) end def mutex @@ -38,6 +38,10 @@ module SiteSettingExtension @enums ||= {} end + def lists + @lists ||= [] + end + def hidden_settings @hidden_settings ||= [] end @@ -56,10 +60,13 @@ module SiteSettingExtension enum = opts[:enum] enums[name] = enum.is_a?(String) ? enum.constantize : enum end - if opts[:hidden] == true + if opts[:list] + lists << name + end + if opts[:hidden] hidden_settings << name end - if opts[:refresh] == true + if opts[:refresh] refresh_settings << name end @@ -261,6 +268,7 @@ module SiteSettingExtension def get_data_type(name,val) return types[:null] if val.nil? return types[:enum] if enums[name] + return types[:list] if lists.include? name case val when String @@ -278,12 +286,14 @@ module SiteSettingExtension case type when types[:fixnum] value.to_i - when types[:string], types[:enum] + when types[:string], types[:list], types[:enum] value when types[:bool] value == true || value == "t" || value == "true" when types[:null] nil + else + raise ArgumentError.new :type end end