diff --git a/Gemfile.lock b/Gemfile.lock index 32ab0c59b2..c9b6f3ddc1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM mail (2.6.6) mime-types (>= 1.16, < 4) memory_profiler (0.9.8) - message_bus (2.0.3) + message_bus (2.0.5) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) @@ -214,7 +214,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.17) + onebox (1.8.18) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index f63b80994f..ac3c82689f 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -236,7 +236,7 @@ export default Ember.Component.extend({ const shortcuts = this.get('toolbar.shortcuts'); // for some reason I am having trouble bubbling this so hack it in - mouseTrap.bind(['ctrl+/','command+/'], (event) =>{ + mouseTrap.bind(['ctrl+shift+s','command+shift+s'], (event) =>{ this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event}); return true; }); @@ -259,7 +259,7 @@ export default Ember.Component.extend({ if (this.get('composerEvents')) { this.appEvents.on('composer:insert-block', text => this._addBlock(this._getSelected(), text)); - this.appEvents.on('composer:insert-text', text => this._addText(this._getSelected(), text)); + this.appEvents.on('composer:insert-text', (text, options) => this._addText(this._getSelected(), text, options)); this.appEvents.on('composer:replace-text', (oldVal, newVal) => this._replaceText(oldVal, newVal)); } this._mouseTrap = mouseTrap; @@ -613,8 +613,22 @@ export default Ember.Component.extend({ Ember.run.scheduleOnce("afterRender", () => $textarea.focus()); }, - _addText(sel, text) { + _addText(sel, text, options) { const $textarea = this.$('textarea.d-editor-input'); + + if (options && options.ensureSpace) { + if ((sel.pre + '').length > 0) { + if (!sel.pre.match(/\s$/)) { + text = ' ' + text; + } + } + if ((sel.post + '').length > 0) { + if (!sel.post.match(/^\s/)) { + text = text + ' '; + } + } + } + const insert = `${sel.pre}${text}`; const value = `${insert}${sel.post}`; this.set('value', value); diff --git a/app/assets/javascripts/discourse/components/footer-message.js.es6 b/app/assets/javascripts/discourse/components/footer-message.js.es6 new file mode 100644 index 0000000000..1d6d852838 --- /dev/null +++ b/app/assets/javascripts/discourse/components/footer-message.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + classNames: ['footer-message'] +}); diff --git a/app/assets/javascripts/discourse/components/queued-post.js.es6 b/app/assets/javascripts/discourse/components/queued-post.js.es6 index bd1e3292d1..b0b40c996b 100644 --- a/app/assets/javascripts/discourse/components/queued-post.js.es6 +++ b/app/assets/javascripts/discourse/components/queued-post.js.es6 @@ -19,13 +19,14 @@ function updateState(state, opts) { export default Ember.Component.extend(bufferedProperty('editables'), { editing: propertyEqual('post', 'currentlyEditing'), - editables: {}, + editables: null, _confirmDelete: updateState('rejected', {deleteUser: true}), _initEditables: function() { const post = this.get('post'); const postOptions = post.get('post_options'); + this.set('editables', {}); this.set('editables.raw', post.get('raw')); this.set('editables.category', post.get('category')); this.set('editables.category_id', post.get('category.id')); diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 1979fa984f..6ae50d0475 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -6,8 +6,8 @@ const bindings = { '!': {postAction: 'showFlags'}, '#': {handler: 'goToPost', anonymous: true}, '/': {handler: 'toggleSearch', anonymous: true}, - 'ctrl+/': {handler: 'toggleSearch', anonymous: true}, - 'command+/': {handler: 'toggleSearch', anonymous: true}, + 'ctrl+shift+s': {handler: 'toggleSearch', anonymous: true}, + 'command+shift+s': {handler: 'toggleSearch', anonymous: true}, '=': {handler: 'toggleHamburgerMenu', anonymous: true}, '?': {handler: 'showHelpModal', anonymous: true}, '.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 9d65972904..19cdd9116d 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -64,7 +64,7 @@ {{d-icon "check"}} {{i18n 'topics.bulk.dismiss_new'}} {{/if}} - {{#footer-message education=footerEducation message=footerMessage tagName=""}} + {{#footer-message education=footerEducation message=footerMessage}} {{#if latest}} {{#if canCreateTopicOnCategory}}{{i18n 'topic.suggest_create_topic'}}{{/if}} {{else if top}} diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index 4eb7503e29..5c07afdc59 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -40,29 +40,15 @@ {{i18n 'topics.bulk.dismiss_new'}} {{/if}} - {{#if latest}} -
- {{{footerEducation}}} -
-

- {{footerMessage}} - {{#if model.can_create_topic}}{{i18n 'topic.suggest_create_topic'}}{{/if}} -

- {{else}} - {{#if top}} -

- {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}} -
- {{top-period-buttons period=period action="changePeriod"}} -

+ {{#footer-message education=footerEducation message=footerMessage}} + {{#if latest}} + {{#if canCreateTopicOnCategory}}{{i18n 'topic.suggest_create_topic'}}{{/if}} + {{else if top}} + {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}} + {{top-period-buttons period=period action="changePeriod"}} {{else}} -
- {{{footerEducation}}} -
-

- {{footerMessage}}{{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} -

+ {{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{/if}} - {{/if}} + {{/footer-message}} {{/if}} diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index c195c962f6..8b10b12e3d 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -20,8 +20,8 @@ class Highlighted extends RawHtml { function createSearchResult({ type, linkField, builder }) { return createWidget(`search-result-${type}`, { html(attrs) { - return attrs.results.map(r => { + return attrs.results.map(r => { let searchResultId; if (type === "topic") { searchResultId = r.get('topic_id'); diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index d5e9b12e9a..b5c0f840f3 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -132,7 +132,7 @@ export default createWidget('search-menu', { noResults: searchData.noResults, results: searchData.results, invalidTerm: searchData.invalidTerm, - searchContextEnabled: searchData.contextEnabled + searchContextEnabled: searchData.contextEnabled, })); } } @@ -169,6 +169,75 @@ export default createWidget('search-menu', { this.sendWidgetAction('toggleSearchMenu'); }, + keyDown(e) { + if (searchData.loading || searchData.noResults) { + return; + } + + if (e.which === 65 /* a */) { + let focused = $('header .results .search-link:focus'); + if (focused.length === 1) { + if ($('#reply-control.open').length === 1) { + // add a link and focus composer + + this.appEvents.trigger('composer:insert-text', focused[0].href, {ensureSpace: true}); + this.appEvents.trigger('header:keyboard-trigger', {type: 'search'}); + + e.preventDefault(); + $('#reply-control.open textarea').focus(); + return false; + } + } + } + + const up = e.which === 38; + const down = e.which === 40; + if (up || down) { + + let focused = $('header .panel-body *:focus')[0]; + + if (!focused) { + return; + } + + let links = $('header .panel-body .results a'); + let results = $('header .panel-body .results .search-link'); + + let prevResult; + let result; + + links.each((idx,item) => { + if ($(item).hasClass('search-link')) { + prevResult = item; + } + + if (item === focused) { + result = prevResult; + } + }); + + let index = -1; + + if (result) { + index = results.index(result); + } + + if (index === -1 && down) { + $('header .panel-body .search-link:first').focus(); + } else if (index === 0 && up) { + $('header .panel-body input:first').focus(); + } else if (index > -1) { + index += (down ? 1 : -1); + if (index >= 0 && index < results.length) { + $(results[index]).focus(); + } + } + + e.preventDefault(); + return false; + } + }, + triggerSearch() { searchData.noResults = false; this.searchService().set('highlightTerm', searchData.term); diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 21e80fb44e..10d2de5b67 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -18,7 +18,7 @@ } .menu-panel { - border: 1px solid $primary-low; + border: 1px solid $primary-low; box-shadow: 0 2px 2px rgba(0,0,0, .25); background-color: $secondary; z-index: 1100; @@ -148,6 +148,7 @@ padding: 5px; text-align: center; } + .filter { padding: 0; &:hover {background: transparent;} diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 07d645809f..b5113638e2 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -92,6 +92,11 @@ .user-table { margin-top: 30px; width: 100%; + display: table; + table-layout: fixed; + .wrapper { + display: table-row; + } } .user-navigation .nav-stacked .glyph { diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index ca69ac2f14..fa99d1f2d9 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -574,6 +574,9 @@ class PostsController < ApplicationController params[:skip_validations] = params[:skip_validations].to_s == "true" permitted << :skip_validations + params[:import_mode] = params[:import_mode].to_s == "true" + permitted << :import_mode + # We allow `embed_url` via the API permitted << :embed_url diff --git a/app/models/backup.rb b/app/models/backup.rb index 72a619a06c..38dd0b2a83 100644 --- a/app/models/backup.rb +++ b/app/models/backup.rb @@ -31,6 +31,7 @@ class Backup def after_create_hook upload_to_s3 if SiteSetting.enable_s3_backups? + DiscourseEvent.trigger(:backup_created) end def after_remove_hook diff --git a/app/models/category_featured_topic.rb b/app/models/category_featured_topic.rb index 22d948dd59..a517064374 100644 --- a/app/models/category_featured_topic.rb +++ b/app/models/category_featured_topic.rb @@ -23,7 +23,13 @@ class CategoryFeaturedTopic < ActiveRecord::Base no_definitions: true } - # Add topics, even if they're in secured categories: + # It may seem a bit odd that we are running 2 queries here, when admin + # can clearly pull out all the topics needed. + # We do so, so anonymous will ALWAYS get some topics + # If we only fetched as admin we may have a situation where anon can see + # no featured topics (all the previous 2x topics are only visible to admins) + + # Add topics, even if they're in secured categories or invisible query = TopicQuery.new(CategoryFeaturedTopic.fake_admin, query_opts) results = query.list_category_topic_ids(c).uniq diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index 139b598e28..25a6422b0e 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -139,7 +139,6 @@ module HasCustomFields end def custom_fields - if @preloaded_custom_fields return @preloaded_proxy ||= PreloadedProxy.new(@preloaded_custom_fields) end @@ -177,7 +176,10 @@ module HasCustomFields dup.delete(f.name) end else - if dup[f.name] != f.value + t = {} + self.class.append_custom_field(t, f.name, f.value) + + if dup[f.name] != t[f.name] f.destroy else dup.delete(f.name) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index bae43779c4..1ce75d44ed 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2376,7 +2376,7 @@ en: hamburger_menu: '= Open hamburger menu' user_profile_menu: 'p Open user menu' show_incoming_updated_topics: '. Show updated topics' - search: '/ or ctrl+/ Search' + search: '/ or ctrl+shift+s Search' help: '? Open keyboard help' dismiss_new_posts: 'x, r Dismiss New/Posts' dismiss_topics: 'x, t Dismiss Topics' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b02a01e761..334e87e7b9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2424,7 +2424,7 @@ en: invited_group_to_private_message_body: | %{username} invited @%{group_name} to a message - > #### %{topic_title} + > **%{topic_title}** > > %{topic_excerpt} diff --git a/lib/discourse.rb b/lib/discourse.rb index f267534b2b..c2aa10a0ea 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -447,7 +447,7 @@ module Discourse def self.reset_active_record_cache ActiveRecord::Base.connection.query_cache.clear - (ActiveRecord::Base.connection.tables - %w[schema_migrations]).each do |table| + (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| table.classify.constantize.reset_column_information rescue nil end nil diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 8207feae75..15db587294 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -92,7 +92,7 @@ class Plugin::Instance def whitelist_staff_user_custom_field(field) reloadable_patch do |plugin| - User.register_plugin_staff_custom_field(field, plugin) if plugin.enabled? + ::User.register_plugin_staff_custom_field(field, plugin) if plugin.enabled? end end @@ -248,21 +248,27 @@ class Plugin::Instance end end + def register_category_custom_field_type(name, type) + reloadable_patch do |plugin| + Category.register_custom_field_type(name, type) if plugin.enabled? + end + end + def register_topic_custom_field_type(name, type) reloadable_patch do |plugin| - Topic.register_custom_field_type(name, type) if plugin.enabled? + ::Topic.register_custom_field_type(name, type) if plugin.enabled? end end def register_post_custom_field_type(name, type) reloadable_patch do |plugin| - Post.register_custom_field_type(name, type) if plugin.enabled? + ::Post.register_custom_field_type(name, type) if plugin.enabled? end end def register_group_custom_field_type(name, type) reloadable_patch do |plugin| - Group.register_custom_field_type(name, type) if plugin.enabled? + ::Group.register_custom_field_type(name, type) if plugin.enabled? end end diff --git a/lib/stylesheet/watcher.rb b/lib/stylesheet/watcher.rb index 27dbe8f692..f0778b38dc 100644 --- a/lib/stylesheet/watcher.rb +++ b/lib/stylesheet/watcher.rb @@ -37,10 +37,14 @@ module Stylesheet end root = Rails.root.to_s + + listener_opts = { ignore: /xxxx/ } + listener_opts[:force_polling] = true if ENV['FORCE_POLLING'] + @paths.each do |watch| Thread.new do begin - listener = Listen.to("#{root}/#{watch}", ignore: /xxxx/) do |modified, added, _| + listener = Listen.to("#{root}/#{watch}", listener_opts) do |modified, added, _| paths = [modified, added].flatten paths.compact! paths.map! { |long| long[(root.length + 1)..-1] } diff --git a/lib/version.rb b/lib/version.rb index 8e27a89f97..bc025b9cb8 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 1 MINOR = 9 TINY = 0 - PRE = 'beta6' + PRE = 'beta7' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/spec/components/concern/has_custom_fields_spec.rb b/spec/components/concern/has_custom_fields_spec.rb index 75ec51dd05..fc1f1a43e8 100644 --- a/spec/components/concern/has_custom_fields_spec.rb +++ b/spec/components/concern/has_custom_fields_spec.rb @@ -142,6 +142,16 @@ describe HasCustomFields do test_item.reload expect(test_item.custom_fields).to eq("bool" => true, "int" => 1, "json" => { "foo" => "bar" }) + + before_ids = CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id) + + test_item.custom_fields["bool"] = false + test_item.save + + after_ids = CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id) + + # we updated only 1 custom field, so there should be only 1 different id + expect((before_ids - after_ids).size).to eq(1) end it "simple modifications don't interfere" do diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index c5ffb51252..c27a7c9e0e 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -510,7 +510,7 @@ describe PostsController do end it "toggle wiki status should create a new version" do - admin = log_in(:admin) + _admin = log_in(:admin) another_user = Fabricate(:user) another_post = Fabricate(:post, user: another_user) @@ -520,7 +520,7 @@ describe PostsController do expect { xhr :put, :wiki, post_id: another_post.id, wiki: 'false' } .to change { another_post.reload.version }.by(-1) - another_admin = log_in(:admin) + _another_admin = log_in(:admin) expect { xhr :put, :wiki, post_id: another_post.id, wiki: 'true' } .to change { another_post.reload.version }.by(1) @@ -631,6 +631,46 @@ describe PostsController do expect(response.body).to eq(original) end + + it 'allows to create posts in import_mode' do + NotificationEmailer.enable + post = Fabricate(:post) + user = Fabricate(:user) + master_key = ApiKey.create_master_key.key + + xhr :post, :create, + api_username: user.username, + api_key: master_key, + raw: 'this is test reply 1', + topic_id: post.topic.id, + reply_to_post_number: 1 + + expect(response).to be_success + expect(post.topic.user.notifications.count).to eq(1) + post.topic.user.notifications.destroy_all + + xhr :post, :create, + api_username: user.username, + api_key: master_key, + raw: 'this is test reply 2', + topic_id: post.topic.id, + reply_to_post_number: 1, + import_mode: true + + expect(response).to be_success + expect(post.topic.user.notifications.count).to eq(0) + + xhr :post, :create, + api_username: user.username, + api_key: master_key, + raw: 'this is test reply 3', + topic_id: post.topic.id, + reply_to_post_number: 1, + import_mode: false + + expect(response).to be_success + expect(post.topic.user.notifications.count).to eq(1) + end end describe 'when logged in' do diff --git a/spec/models/category_featured_topic_spec.rb b/spec/models/category_featured_topic_spec.rb index ee102375d4..7c9a611e0f 100644 --- a/spec/models/category_featured_topic_spec.rb +++ b/spec/models/category_featured_topic_spec.rb @@ -34,7 +34,7 @@ describe CategoryFeaturedTopic do it 'should feature stuff in the correct order' do category = Fabricate(:category, num_featured_topics: 2) - t5 = Fabricate(:topic, category_id: category.id, bumped_at: 12.minutes.ago) + _t5 = Fabricate(:topic, category_id: category.id, bumped_at: 12.minutes.ago) t4 = Fabricate(:topic, category_id: category.id, bumped_at: 10.minutes.ago) t3 = Fabricate(:topic, category_id: category.id, bumped_at: 7.minutes.ago) t2 = Fabricate(:topic, category_id: category.id, bumped_at: 4.minutes.ago) @@ -45,7 +45,7 @@ describe CategoryFeaturedTopic do # Should find more than we need: pinned topics first, then num_featured_topics * 2 expect( - CategoryFeaturedTopic.where(category_id: category.id).pluck(:topic_id) + CategoryFeaturedTopic.where(category_id: category.id).order('rank asc').pluck(:topic_id) ).to eq([pinned.id, t2.id, t1.id, t3.id, t4.id]) end