From f9ff06b9d4af97612c679d911e17137ac401034e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 16 Aug 2017 12:59:21 -0400 Subject: [PATCH 01/21] Allow ENV variable to force polling --- lib/stylesheet/watcher.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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] } From 304379d4368f6f931fc3aa12d4e7c14dae97ee3d Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 16 Aug 2017 14:14:44 -0400 Subject: [PATCH 02/21] FEATURE: up/down arrow will highlight search result Then you can use enter to visit result. --- .../widgets/search-menu-results.js.es6 | 12 ++- .../discourse/widgets/search-menu.js.es6 | 81 ++++++++++++++++++- .../stylesheets/common/base/menu-panel.scss | 9 ++- 3 files changed, 96 insertions(+), 6 deletions(-) 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..767f192c87 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -20,13 +20,18 @@ class Highlighted extends RawHtml { function createSearchResult({ type, linkField, builder }) { return createWidget(`search-result-${type}`, { html(attrs) { - return attrs.results.map(r => { + let i=-1; + + return attrs.results.map(r => { + i+=1; let searchResultId; if (type === "topic") { searchResultId = r.get('topic_id'); } - return h('li', this.attach('link', { + let className = i === attrs.selected ? '.selected' : ''; + + return h('li' + className, { attributes: { tabindex: '-1' } }, this.attach('link', { href: r.get(linkField), contents: () => builder.call(this, r, attrs.term), className: 'search-link', @@ -126,7 +131,8 @@ createWidget('search-menu-results', { searchContextEnabled: attrs.searchContextEnabled, searchLogId: attrs.results.grouped_search_result.search_log_id, results: rt.results, - term: attrs.term + term: attrs.term, + selected: (attrs.selected && attrs.selected.type === rt.type) ? attrs.selected.index : -1 })), h('div.no-results', more) ]; diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index d5e9b12e9a..08aa8f1b11 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -9,7 +9,8 @@ const searchData = { noResults: false, term: undefined, typeFilter: null, - invalidTerm: false + invalidTerm: false, + selected: null }; // Helps with debouncing and cancelling promises @@ -132,7 +133,8 @@ export default createWidget('search-menu', { noResults: searchData.noResults, results: searchData.results, invalidTerm: searchData.invalidTerm, - searchContextEnabled: searchData.contextEnabled + searchContextEnabled: searchData.contextEnabled, + selected: searchData.selected })); } } @@ -169,6 +171,81 @@ export default createWidget('search-menu', { this.sendWidgetAction('toggleSearchMenu'); }, + keyDown(e) { + if (searchData.loading || searchData.noResults) { + return; + } + + if (e.which === 13 /*enter*/ && searchData.selected) { + searchData.selected = null; + $('header .results li.selected a').click(); + } + + if (e.which === 38 /*arrow up*/ || e.which === 40 /*arrow down*/) { + this.moveSelected(e.which === 38 ? -1 : 1); + + this.scheduleRerender(); + + Em.run.next(()=>{ + if (searchData.selected) { + + // so we do not clear selected + $('header .results li').off('blur'); + + let selected = $('header .results li.selected') + .focus() + .on('blur', ()=> { + searchData.selected = null; + this.scheduleRerender(); + selected.off('blur'); + }); + + } else { + $('#search-term').focus(); + } + }); + + e.preventDefault(); + return false; + } + }, + + moveSelected(offset) { + + if (offset === 1 && !searchData.selected) { + searchData.selected = {type: searchData.results.resultTypes[0].type, index: 0}; + return; + } + + if (!searchData.selected) { + return; + } + + let typeIndex = _.findIndex(searchData.results.resultTypes, item => item.type === searchData.selected.type); + + if (typeIndex === 0 && searchData.selected.index === 0 && offset === -1) { + searchData.selected = null; + return; + } + + let currentResults = searchData.results.resultTypes[typeIndex].results; + let newPosition = searchData.selected.index + offset; + + if (newPosition < currentResults.length && newPosition >= 0) { + searchData.selected.index = newPosition; + } else { + // possibly move to next type + let newTypeIndex = typeIndex + offset; + if (newTypeIndex >= 0 && newTypeIndex < searchData.results.resultTypes.length) { + newPosition = 0; + if (offset === -1) { + newPosition = searchData.results.resultTypes[newTypeIndex].results.length - 1; + } + searchData.selected = {type: searchData.results.resultTypes[newTypeIndex].type, index: newPosition}; + } + } + }, + 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..09dde7b8d0 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,13 @@ padding: 5px; text-align: center; } + + li.selected { + background-color: $highlight-medium; + } + li:focus { + outline: none; + } .filter { padding: 0; &:hover {background: transparent;} From 91f0f76fb1a40bbd4dd6dd46dd4e5bb9663d0f82 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 16 Aug 2017 15:05:39 -0400 Subject: [PATCH 03/21] update message bus to ruby 2.0 packaged version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 32ab0c59b2..e3a43e7bec 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) From 55f449edc53b334d8b89a49305377a66dba8d4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 16 Aug 2017 23:00:52 +0200 Subject: [PATCH 04/21] FIX: reloading issues with classes --- lib/plugin/instance.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 8207feae75..a3caa4822c 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 @@ -250,19 +250,19 @@ class Plugin::Instance 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 From 629810bd0742775bed54ee0050d33f190716da98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 16 Aug 2017 23:04:40 +0200 Subject: [PATCH 05/21] FIX: when registering custom_field types, ensure we're casting them before checking for equality --- app/models/concerns/has_custom_fields.rb | 6 ++++-- spec/components/concern/has_custom_fields_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) 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/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 From 8f795b35bb18cebb01e1853ce642d78262a3af64 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 16 Aug 2017 16:43:14 -0700 Subject: [PATCH 06/21] missed a spot on email invite h4 to bold conversion --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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} From 43e2c91845cbbe8490a7d0914a0143b0b95ccff6 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Thu, 17 Aug 2017 02:52:50 -0400 Subject: [PATCH 07/21] fixes word wrap issue on user activity page (#5056) By default, the width of a table and its cells are adjusted to fit the content. Thus, longer words are not wrapped and lead to a wider width. --- app/assets/stylesheets/desktop/user.scss | 5 +++++ 1 file changed, 5 insertions(+) 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 { From 2157079d0926f78597f9e2928801137c77b9ab0f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 17 Aug 2017 15:59:31 +0900 Subject: [PATCH 08/21] Add Plugin API to register a category custom field. --- lib/plugin/instance.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index a3caa4822c..15db587294 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -248,6 +248,12 @@ 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? From eafab41a2cfe8764e9be7417dd1bcb4c035210d3 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 17 Aug 2017 13:10:11 +0530 Subject: [PATCH 09/21] bump onebox version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e3a43e7bec..c9b6f3ddc1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) From 0e656ff213d11b0d416927763c284fa98f91aa5b Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 17 Aug 2017 19:27:35 +0900 Subject: [PATCH 10/21] FIX: Can't reset AR schema cache due to versions table. --- lib/discourse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 84c83afd3508efb431b84236124f30411c5d81b1 Mon Sep 17 00:00:00 2001 From: Mudasir Raza Date: Thu, 17 Aug 2017 16:53:04 +0500 Subject: [PATCH 11/21] Allow optional import_mode param for posts in api (#4952) --- app/controllers/posts_controller.rb | 3 +++ spec/controllers/posts_controller_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) 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/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index c5ffb51252..c068fa9754 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -631,6 +631,26 @@ 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 From aea9db56d4d4b0ff0b394cf800066f6b7d2d0242 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 17 Aug 2017 08:10:57 -0400 Subject: [PATCH 12/21] fix formatting --- spec/controllers/posts_controller_spec.rb | 30 +++++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index c068fa9754..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) @@ -638,16 +638,36 @@ describe PostsController do 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} + 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} + 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} + 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 From b5823a3b037a357a0f0eb03289bc41f726c9fe1b Mon Sep 17 00:00:00 2001 From: Jen Date: Thu, 17 Aug 2017 15:44:00 +0200 Subject: [PATCH 13/21] add event for other backup choices --- app/models/backup.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/backup.rb b/app/models/backup.rb index 72a619a06c..02a9e6e398 100644 --- a/app/models/backup.rb +++ b/app/models/backup.rb @@ -30,7 +30,11 @@ class Backup end def after_create_hook - upload_to_s3 if SiteSetting.enable_s3_backups? + if SiteSetting.enable_s3_backups? + upload_to_s3 + else + DiscourseEvent.trigger(:backup_choice) + end end def after_remove_hook From d29fc781fbb744c07f67eabb89d39289f98db46a Mon Sep 17 00:00:00 2001 From: Jen Date: Thu, 17 Aug 2017 16:24:56 +0200 Subject: [PATCH 14/21] trigger for other backup choices --- app/models/backup.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/models/backup.rb b/app/models/backup.rb index 02a9e6e398..38dd0b2a83 100644 --- a/app/models/backup.rb +++ b/app/models/backup.rb @@ -30,11 +30,8 @@ class Backup end def after_create_hook - if SiteSetting.enable_s3_backups? - upload_to_s3 - else - DiscourseEvent.trigger(:backup_choice) - end + upload_to_s3 if SiteSetting.enable_s3_backups? + DiscourseEvent.trigger(:backup_created) end def after_remove_hook From dc4d5677eb1d0297c2b42dc754d8bd49a0222db0 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Aug 2017 12:39:43 -0400 Subject: [PATCH 15/21] FEATURE: use `a` when search result is focused to add to composer --- .../discourse/components/d-editor.js.es6 | 18 ++- .../widgets/search-menu-results.js.es6 | 10 +- .../discourse/widgets/search-menu.js.es6 | 107 ++++++++---------- .../stylesheets/common/base/menu-panel.scss | 6 - 4 files changed, 66 insertions(+), 75 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index f63b80994f..574d3b49b5 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -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/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index 767f192c87..8b10b12e3d 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -21,17 +21,12 @@ function createSearchResult({ type, linkField, builder }) { return createWidget(`search-result-${type}`, { html(attrs) { - let i=-1; - return attrs.results.map(r => { - i+=1; let searchResultId; if (type === "topic") { searchResultId = r.get('topic_id'); } - let className = i === attrs.selected ? '.selected' : ''; - - return h('li' + className, { attributes: { tabindex: '-1' } }, this.attach('link', { + return h('li', this.attach('link', { href: r.get(linkField), contents: () => builder.call(this, r, attrs.term), className: 'search-link', @@ -131,8 +126,7 @@ createWidget('search-menu-results', { searchContextEnabled: attrs.searchContextEnabled, searchLogId: attrs.results.grouped_search_result.search_log_id, results: rt.results, - term: attrs.term, - selected: (attrs.selected && attrs.selected.type === rt.type) ? attrs.selected.index : -1 + term: attrs.term })), h('div.no-results', more) ]; diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index 08aa8f1b11..3da7074674 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -9,8 +9,7 @@ const searchData = { noResults: false, term: undefined, typeFilter: null, - invalidTerm: false, - selected: null + invalidTerm: false }; // Helps with debouncing and cancelling promises @@ -134,7 +133,6 @@ export default createWidget('search-menu', { results: searchData.results, invalidTerm: searchData.invalidTerm, searchContextEnabled: searchData.contextEnabled, - selected: searchData.selected })); } } @@ -176,76 +174,67 @@ export default createWidget('search-menu', { return; } - if (e.which === 13 /*enter*/ && searchData.selected) { - searchData.selected = null; - $('header .results li.selected a').click(); + 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}); + + e.preventDefault(); + $('#reply-control.open textarea').focus(); + return false; + } + } } - if (e.which === 38 /*arrow up*/ || e.which === 40 /*arrow down*/) { - this.moveSelected(e.which === 38 ? -1 : 1); + const up = e.which === 38; + const down = e.which === 40; + if (up || down) { - this.scheduleRerender(); + let focused = $('header .panel-body *:focus')[0]; - Em.run.next(()=>{ - if (searchData.selected) { + if (!focused) { + return; + } - // so we do not clear selected - $('header .results li').off('blur'); + let links = $('header .panel-body .results a'); + let results = $('header .panel-body .results .search-link'); - let selected = $('header .results li.selected') - .focus() - .on('blur', ()=> { - searchData.selected = null; - this.scheduleRerender(); - selected.off('blur'); - }); + let prevResult; + let result; - } else { - $('#search-term').focus(); - } + 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 > -1) { + index += (down ? 1 : -1); + if (index >= 0 && index < results.length) { + $(results[index]).focus(); + } + } + e.preventDefault(); return false; } }, - moveSelected(offset) { - - if (offset === 1 && !searchData.selected) { - searchData.selected = {type: searchData.results.resultTypes[0].type, index: 0}; - return; - } - - if (!searchData.selected) { - return; - } - - let typeIndex = _.findIndex(searchData.results.resultTypes, item => item.type === searchData.selected.type); - - if (typeIndex === 0 && searchData.selected.index === 0 && offset === -1) { - searchData.selected = null; - return; - } - - let currentResults = searchData.results.resultTypes[typeIndex].results; - let newPosition = searchData.selected.index + offset; - - if (newPosition < currentResults.length && newPosition >= 0) { - searchData.selected.index = newPosition; - } else { - // possibly move to next type - let newTypeIndex = typeIndex + offset; - if (newTypeIndex >= 0 && newTypeIndex < searchData.results.resultTypes.length) { - newPosition = 0; - if (offset === -1) { - newPosition = searchData.results.resultTypes[newTypeIndex].results.length - 1; - } - searchData.selected = {type: searchData.results.resultTypes[newTypeIndex].type, index: newPosition}; - } - } - }, - 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 09dde7b8d0..10d2de5b67 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -149,12 +149,6 @@ text-align: center; } - li.selected { - background-color: $highlight-medium; - } - li:focus { - outline: none; - } .filter { padding: 0; &:hover {background: transparent;} From 889b99552aa8d5bbf61b458391f3125c31f42bb0 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Aug 2017 12:57:08 -0400 Subject: [PATCH 16/21] FEATURE: allow ctrl+shift+s to open search --- app/assets/javascripts/discourse/components/d-editor.js.es6 | 2 +- .../javascripts/discourse/lib/keyboard-shortcuts.js.es6 | 4 ++-- app/assets/javascripts/discourse/widgets/search-menu.js.es6 | 1 + config/locales/client.en.yml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 574d3b49b5..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; }); 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/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index 3da7074674..76ff9a0527 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -181,6 +181,7 @@ export default createWidget('search-menu', { // 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(); 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' From 9246bc340061d4e47db0e2ca359683c891a7ca8b Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Aug 2017 13:32:30 -0400 Subject: [PATCH 17/21] support up button from first result --- app/assets/javascripts/discourse/widgets/search-menu.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index 76ff9a0527..b5c0f840f3 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -224,6 +224,8 @@ export default createWidget('search-menu', { 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) { From 4b0dcd64ebbbc1296eae11087a798477b2e01df1 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 17 Aug 2017 14:39:42 -0400 Subject: [PATCH 18/21] FIX: Footer message was missing the class --- .../components/footer-message.js.es6 | 3 ++ .../discourse/templates/discovery/topics.hbs | 2 +- .../templates/mobile/discovery/topics.hbs | 30 +++++-------------- 3 files changed, 12 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/footer-message.js.es6 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/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}} From 0c4527323e5bdfc9137dec2e1cfa37b26d381906 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Aug 2017 14:46:59 -0400 Subject: [PATCH 19/21] FIX: queues posts broken for multiple posts --- app/assets/javascripts/discourse/components/queued-post.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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')); From c2a6616035c34af4a7ee5a7f5d58a3165796d36c Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Aug 2017 15:26:31 -0400 Subject: [PATCH 20/21] Correct erratic spec failure --- app/models/category_featured_topic.rb | 8 +++++++- spec/models/category_featured_topic_spec.rb | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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/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 From df5142493c99908aca55e183b5681477e9c4871d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 17 Aug 2017 15:59:29 -0400 Subject: [PATCH 21/21] Version bump to v1.9.0.beta7 --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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