From 5059dad8f0baebc602f3b4333b03a1d807488a15 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 25 Jul 2018 23:24:43 +0530 Subject: [PATCH 001/168] FEATURE: Webhook for post approval events --- app/models/queued_post.rb | 7 +++++ app/models/web_hook_event_type.rb | 1 + config/initializers/012-web_hook_events.rb | 10 +++++++ config/locales/client.en.yml | 3 ++ db/fixtures/007_web_hook_event_types.rb | 5 ++++ spec/fabricators/web_hook_fabricator.rb | 8 ++++++ spec/models/queued_post_spec.rb | 33 ++++++++++++++++++++++ spec/models/web_hook_spec.rb | 24 ++++++++++++++++ 8 files changed, 91 insertions(+) diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index 1f2afa75fe..568307825d 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -7,6 +7,8 @@ class QueuedPost < ActiveRecord::Base belongs_to :approved_by, class_name: "User" belongs_to :rejected_by, class_name: "User" + after_commit :trigger_queued_post_event, on: :create + def create_pending_action UserAction.log_action!(action_type: UserAction::PENDING, user_id: user_id, @@ -15,6 +17,11 @@ class QueuedPost < ActiveRecord::Base queued_post_id: id) end + def trigger_queued_post_event + DiscourseEvent.trigger(:queued_post, self) + true + end + def self.states @states ||= Enum.new(:new, :approved, :rejected) end diff --git a/app/models/web_hook_event_type.rb b/app/models/web_hook_event_type.rb index 7d6d595566..7ba6569711 100644 --- a/app/models/web_hook_event_type.rb +++ b/app/models/web_hook_event_type.rb @@ -6,6 +6,7 @@ class WebHookEventType < ActiveRecord::Base CATEGORY = 5 TAG = 6 FLAG = 7 + APPROVAL = 8 has_and_belongs_to_many :web_hooks diff --git a/config/initializers/012-web_hook_events.rb b/config/initializers/012-web_hook_events.rb index d96857c723..f9b57d9570 100644 --- a/config/initializers/012-web_hook_events.rb +++ b/config/initializers/012-web_hook_events.rb @@ -88,3 +88,13 @@ end WebHook.enqueue_object_hooks(:flag, flag, event) end end + +%i( + queued_post + approved_post + rejected_post +).each do |event| + DiscourseEvent.on(event) do |queued_post| + WebHook.enqueue_object_hooks(:approval, queued_post, event, QueuedPostSerializer) + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fd8245a8fa..75e4c616c1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3027,6 +3027,9 @@ en: flag_event: name: "Flag Event" details: "When a flag is created, agreed, disagreed or ignored." + approval_event: + name: "Approval Event" + details: "When a new post is queued, approved or rejected." delivery_status: title: "Delivery Status" inactive: "Inactive" diff --git a/db/fixtures/007_web_hook_event_types.rb b/db/fixtures/007_web_hook_event_types.rb index 8aa0859160..4b3291eb8d 100644 --- a/db/fixtures/007_web_hook_event_types.rb +++ b/db/fixtures/007_web_hook_event_types.rb @@ -32,3 +32,8 @@ WebHookEventType.seed do |b| b.id = WebHookEventType::FLAG b.name = "flag" end + +WebHookEventType.seed do |b| + b.id = WebHookEventType::APPROVAL + b.name = "approval" +end diff --git a/spec/fabricators/web_hook_fabricator.rb b/spec/fabricators/web_hook_fabricator.rb index c7632cd2d0..300fa35e65 100644 --- a/spec/fabricators/web_hook_fabricator.rb +++ b/spec/fabricators/web_hook_fabricator.rb @@ -76,3 +76,11 @@ Fabricator(:flag_web_hook, from: :web_hook) do web_hook.web_hook_event_types = [transients[:flag_hook]] end end + +Fabricator(:approval_web_hook, from: :web_hook) do + transient approval_hook: WebHookEventType.find_by(name: 'approval') + + after_build do |web_hook, transients| + web_hook.web_hook_event_types = [transients[:approval_hook]] + end +end diff --git a/spec/models/queued_post_spec.rb b/spec/models/queued_post_spec.rb index 5616ef41a0..1b22d9f6b3 100644 --- a/spec/models/queued_post_spec.rb +++ b/spec/models/queued_post_spec.rb @@ -165,4 +165,37 @@ describe QueuedPost do end end + describe 'create' do + subject { Fabricate.build(:queued_post) } + + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { subject.save! }.first + + expect(event[:event_name]).to eq(:queued_post) + expect(event[:params].first).to eq(subject) + end + end + + describe 'approve' do + subject { Fabricate(:queued_post) } + + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { subject.approve!(Discourse.system_user) }.last + + expect(event[:event_name]).to eq(:approved_post) + expect(event[:params].first).to eq(subject) + end + end + + describe 'reject' do + subject { Fabricate(:queued_post) } + + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { subject.reject!(Discourse.system_user) }.last + + expect(event[:event_name]).to eq(:rejected_post) + expect(event[:params].first).to eq(subject) + end + end + end diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb index 2e456118b1..034031585e 100644 --- a/spec/models/web_hook_spec.rb +++ b/spec/models/web_hook_spec.rb @@ -417,5 +417,29 @@ describe WebHook do payload = JSON.parse(job_args["payload"]) expect(payload["id"]).to eq(post_action.id) end + + it 'should enqueue the right hooks for post approval events' do + Fabricate(:approval_web_hook) + queued_post = Fabricate(:queued_post) + job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first + + expect(job_args["event_name"]).to eq("queued_post") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(queued_post.id) + + queued_post.approve!(Discourse.system_user) + job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first + + expect(job_args["event_name"]).to eq("approved_post") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(queued_post.id) + + queued_post.reject!(Discourse.system_user) + job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first + + expect(job_args["event_name"]).to eq("rejected_post") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(queued_post.id) + end end end From af5b88f8e25259e6c0f1ecbb4ce72e1a3320567e Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Thu, 26 Jul 2018 10:29:38 +0530 Subject: [PATCH 002/168] Rename approval web hook event type to queued post --- app/models/queued_post.rb | 2 +- app/models/web_hook_event_type.rb | 2 +- config/initializers/012-web_hook_events.rb | 4 ++-- config/locales/client.en.yml | 6 +++--- db/fixtures/007_web_hook_event_types.rb | 4 ++-- spec/fabricators/web_hook_fabricator.rb | 6 +++--- spec/models/queued_post_spec.rb | 2 +- spec/models/web_hook_spec.rb | 6 +++--- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index 568307825d..a4067f8c57 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -18,7 +18,7 @@ class QueuedPost < ActiveRecord::Base end def trigger_queued_post_event - DiscourseEvent.trigger(:queued_post, self) + DiscourseEvent.trigger(:queued_post_created, self) true end diff --git a/app/models/web_hook_event_type.rb b/app/models/web_hook_event_type.rb index 7ba6569711..8841bb64e4 100644 --- a/app/models/web_hook_event_type.rb +++ b/app/models/web_hook_event_type.rb @@ -6,7 +6,7 @@ class WebHookEventType < ActiveRecord::Base CATEGORY = 5 TAG = 6 FLAG = 7 - APPROVAL = 8 + QUEUED_POST = 8 has_and_belongs_to_many :web_hooks diff --git a/config/initializers/012-web_hook_events.rb b/config/initializers/012-web_hook_events.rb index f9b57d9570..0834b875f6 100644 --- a/config/initializers/012-web_hook_events.rb +++ b/config/initializers/012-web_hook_events.rb @@ -90,11 +90,11 @@ end end %i( - queued_post + queued_post_created approved_post rejected_post ).each do |event| DiscourseEvent.on(event) do |queued_post| - WebHook.enqueue_object_hooks(:approval, queued_post, event, QueuedPostSerializer) + WebHook.enqueue_object_hooks(:queued_post, queued_post, event, QueuedPostSerializer) end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 75e4c616c1..1d29338f30 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3027,9 +3027,9 @@ en: flag_event: name: "Flag Event" details: "When a flag is created, agreed, disagreed or ignored." - approval_event: - name: "Approval Event" - details: "When a new post is queued, approved or rejected." + queued_post_event: + name: "Post Approval Event" + details: "When a new queued post is created, approved or rejected." delivery_status: title: "Delivery Status" inactive: "Inactive" diff --git a/db/fixtures/007_web_hook_event_types.rb b/db/fixtures/007_web_hook_event_types.rb index 4b3291eb8d..7e94839fd4 100644 --- a/db/fixtures/007_web_hook_event_types.rb +++ b/db/fixtures/007_web_hook_event_types.rb @@ -34,6 +34,6 @@ WebHookEventType.seed do |b| end WebHookEventType.seed do |b| - b.id = WebHookEventType::APPROVAL - b.name = "approval" + b.id = WebHookEventType::QUEUED_POST + b.name = "queued_post" end diff --git a/spec/fabricators/web_hook_fabricator.rb b/spec/fabricators/web_hook_fabricator.rb index 300fa35e65..0197f14387 100644 --- a/spec/fabricators/web_hook_fabricator.rb +++ b/spec/fabricators/web_hook_fabricator.rb @@ -77,10 +77,10 @@ Fabricator(:flag_web_hook, from: :web_hook) do end end -Fabricator(:approval_web_hook, from: :web_hook) do - transient approval_hook: WebHookEventType.find_by(name: 'approval') +Fabricator(:queued_post_web_hook, from: :web_hook) do + transient queued_post_hook: WebHookEventType.find_by(name: 'queued_post') after_build do |web_hook, transients| - web_hook.web_hook_event_types = [transients[:approval_hook]] + web_hook.web_hook_event_types = [transients[:queued_post_hook]] end end diff --git a/spec/models/queued_post_spec.rb b/spec/models/queued_post_spec.rb index 1b22d9f6b3..23c7dd8bdf 100644 --- a/spec/models/queued_post_spec.rb +++ b/spec/models/queued_post_spec.rb @@ -171,7 +171,7 @@ describe QueuedPost do it 'triggers a extensibility event' do event = DiscourseEvent.track_events { subject.save! }.first - expect(event[:event_name]).to eq(:queued_post) + expect(event[:event_name]).to eq(:queued_post_created) expect(event[:params].first).to eq(subject) end end diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb index 034031585e..d184b973c3 100644 --- a/spec/models/web_hook_spec.rb +++ b/spec/models/web_hook_spec.rb @@ -418,12 +418,12 @@ describe WebHook do expect(payload["id"]).to eq(post_action.id) end - it 'should enqueue the right hooks for post approval events' do - Fabricate(:approval_web_hook) + it 'should enqueue the right hooks for queued post events' do + Fabricate(:queued_post_web_hook) queued_post = Fabricate(:queued_post) job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first - expect(job_args["event_name"]).to eq("queued_post") + expect(job_args["event_name"]).to eq("queued_post_created") payload = JSON.parse(job_args["payload"]) expect(payload["id"]).to eq(queued_post.id) From 330cf78c83a19b5c29afd77b81096bc0c94e80cc Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 26 Jul 2018 14:59:28 -0400 Subject: [PATCH 003/168] =?UTF-8?q?FIX:=20don=E2=80=99t=20break=20browser?= =?UTF-8?q?=20history=20on=20dashboard=20visit=20(#6186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin-dashboard-next-moderation.js.es6 | 2 -- .../admin/models/admin-dashboard-next.js.es6 | 2 +- .../admin/routes/admin-dashboard-next.js.es6 | 14 -------------- .../admin/routes/admin-route-map.js.es6 | 7 +++++-- .../javascripts/admin/templates/dashboard_next.hbs | 2 +- .../stylesheets/common/admin/dashboard_next.scss | 2 +- app/controllers/admin/dashboard_next_controller.rb | 3 +++ config/routes.rb | 6 ++++-- ...eneral.js.es6 => dashboard-next-general.js.es6} | 0 test/javascripts/fixtures/dashboard-next.js.es6 | 2 +- 10 files changed, 16 insertions(+), 24 deletions(-) rename test/javascripts/fixtures/{admin-general.js.es6 => dashboard-next-general.js.es6} (100%) diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 index 0958bc9d62..afe81cbf51 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 @@ -2,8 +2,6 @@ import computed from "ember-addons/ember-computed-decorators"; import PeriodComputationMixin from "admin/mixins/period-computation"; export default Ember.Controller.extend(PeriodComputationMixin, { - exceptionController: Ember.inject.controller("exception"), - @computed flagsStatusOptions() { return { diff --git a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 index 5ad85c0399..6898f8191a 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 @@ -6,7 +6,7 @@ const AdminDashboardNext = Discourse.Model.extend({}); AdminDashboardNext.reopenClass({ fetch() { - return ajax("/admin/dashboard-next.json").then(json => { + return ajax("/admin/dashboard.json").then(json => { const model = AdminDashboardNext.create(); model.set("version_check", json.version_check); return model; diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 index 5aa907b55c..b2ace398fe 100644 --- a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 @@ -5,19 +5,5 @@ export default Discourse.Route.extend({ this.controllerFor("admin-dashboard-next").fetchProblems(); this.controllerFor("admin-dashboard-next").fetchDashboard(); scrollTop(); - }, - - afterModel(model, transition) { - if (transition.targetName === "admin.dashboardNext.index") { - this.transitionTo("admin.dashboardNext.general"); - } - }, - - actions: { - willTransition(transition) { - if (transition.targetName === "admin.dashboardNext.index") { - this.transitionTo("admin.dashboardNext.general"); - } - } } }); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index bdcdcd4a96..110ec6231b 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -3,8 +3,11 @@ export default function() { this.route("dashboard", { path: "/dashboard-old" }); this.route("dashboardNext", { path: "/" }, function() { - this.route("general", { path: "/dashboard/general" }); - this.route("moderation", { path: "/dashboard/moderation" }); + this.route("general", { path: "/" }); + this.route("admin.dashboardNextModeration", { + path: "/dashboard/moderation", + resetNamespace: true + }); }); this.route( diff --git a/app/assets/javascripts/admin/templates/dashboard_next.hbs b/app/assets/javascripts/admin/templates/dashboard_next.hbs index 737593ea3e..176f90c22d 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next.hbs @@ -17,7 +17,7 @@ {{/link-to}} diff --git a/app/assets/stylesheets/common/admin/dashboard_next.scss b/app/assets/stylesheets/common/admin/dashboard_next.scss index c30159ef3f..116e2068f7 100644 --- a/app/assets/stylesheets/common/admin/dashboard_next.scss +++ b/app/assets/stylesheets/common/admin/dashboard_next.scss @@ -31,7 +31,7 @@ border-bottom: 10px solid $secondary; } - &.moderation .navigation-item.moderation { + &.dashboard-next-moderation .navigation-item.moderation { @include active-navigation-item; } diff --git a/app/controllers/admin/dashboard_next_controller.rb b/app/controllers/admin/dashboard_next_controller.rb index d2dc8e79a2..a479e20175 100644 --- a/app/controllers/admin/dashboard_next_controller.rb +++ b/app/controllers/admin/dashboard_next_controller.rb @@ -11,6 +11,9 @@ class Admin::DashboardNextController < Admin::AdminController render json: data end + def moderation + end + def general data = AdminDashboardNextGeneralData.fetch_cached_stats diff --git a/config/routes.rb b/config/routes.rb index 3b2e17dd22..3aa24380b5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -236,9 +236,11 @@ Discourse::Application.routes.draw do get "version_check" => "versions#show" - get "dashboard-next" => "dashboard_next#index" - get "dashboard-old" => "dashboard#index" + get "dashboard" => "dashboard_next#index" get "dashboard/general" => "dashboard_next#general" + get "dashboard/moderation" => "dashboard_next#moderation" + + get "dashboard-old" => "dashboard#index" resources :dashboard, only: [:index] do collection do diff --git a/test/javascripts/fixtures/admin-general.js.es6 b/test/javascripts/fixtures/dashboard-next-general.js.es6 similarity index 100% rename from test/javascripts/fixtures/admin-general.js.es6 rename to test/javascripts/fixtures/dashboard-next-general.js.es6 diff --git a/test/javascripts/fixtures/dashboard-next.js.es6 b/test/javascripts/fixtures/dashboard-next.js.es6 index 257c435089..d284b2eb29 100644 --- a/test/javascripts/fixtures/dashboard-next.js.es6 +++ b/test/javascripts/fixtures/dashboard-next.js.es6 @@ -1,5 +1,5 @@ export default { - "/admin/dashboard-next.json": { + "/admin/dashboard.json": { updated_at: "2018-04-25T08:06:11.292Z" } }; From 135c803f492f13b2b93bf62bce3a169abd5f6af6 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 26 Jul 2018 15:12:12 -0400 Subject: [PATCH 004/168] FIX: don't send PM if flagged post is deleted but flags were deferred or cleared --- lib/post_destroyer.rb | 2 +- spec/components/post_destroyer_spec.rb | 10 ++++++++++ spec/models/post_spec.rb | 25 +++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 7d70b8ecb7..33c316007a 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -196,7 +196,7 @@ class PostDestroyer end def agree_with_flags - if @post.is_flagged? && @user.id > 0 && @user.staff? + if @post.has_active_flag? && @user.id > 0 && @user.staff? Jobs.enqueue( :send_system_message, user_id: @post.user.id, diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 9c81366a0d..f1c772478f 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -606,6 +606,16 @@ describe PostDestroyer do Topic.where(title: I18n.t('system_messages.flags_agreed_and_post_deleted.subject_template')).exists? ).to eq(false) end + + it "should not send the flags_agreed_and_post_deleted message if flags were deferred" do + second_post.expects(:update_flagged_posts_count) + PostAction.defer_flags!(second_post, moderator) + second_post.reload + PostDestroyer.new(moderator, second_post).destroy + expect( + Topic.where(title: I18n.t('system_messages.flags_agreed_and_post_deleted.subject_template')).exists? + ).to eq(false) + end end describe "user actions" do diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 9ea0db26dc..99dbfe7fb4 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -141,7 +141,7 @@ describe Post do let(:user) { Fabricate(:coding_horror) } let(:admin) { Fabricate(:admin) } - it 'isFlagged is accurate' do + it 'is_flagged? is accurate' do PostAction.act(user, post, PostActionType.types[:off_topic]) post.reload expect(post.is_flagged?).to eq(true) @@ -151,7 +151,21 @@ describe Post do expect(post.is_flagged?).to eq(false) end - it 'has_active_flag is accurate' do + it 'is_flagged? is true if flag was deferred' do + PostAction.act(user, post, PostActionType.types[:off_topic]) + PostAction.defer_flags!(post.reload, admin) + post.reload + expect(post.is_flagged?).to eq(true) + end + + it 'is_flagged? is true if flag was cleared' do + PostAction.act(user, post, PostActionType.types[:off_topic]) + PostAction.clear_flags!(post.reload, admin) + post.reload + expect(post.is_flagged?).to eq(true) + end + + it 'has_active_flag? is false for deferred flags' do PostAction.act(user, post, PostActionType.types[:spam]) post.reload expect(post.has_active_flag?).to eq(true) @@ -160,6 +174,13 @@ describe Post do post.reload expect(post.has_active_flag?).to eq(false) end + + it 'has_active_flag? is false for cleared flags' do + PostAction.act(user, post, PostActionType.types[:spam]) + PostAction.clear_flags!(post.reload, admin) + post.reload + expect(post.has_active_flag?).to eq(false) + end end describe "maximum images" do From 56ae826efb3e39163d611bed1ac77233dbe6e050 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 26 Jul 2018 15:26:53 -0400 Subject: [PATCH 005/168] UI: improves no data notice in reports (#6187) --- .../admin/templates/components/admin-report.hbs | 1 + app/assets/stylesheets/common/admin/admin_report.scss | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/app/assets/javascripts/admin/templates/components/admin-report.hbs b/app/assets/javascripts/admin/templates/components/admin-report.hbs index 2f0429854e..6208c7796a 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report.hbs @@ -73,6 +73,7 @@ {{/if}} {{else}}
+ {{d-icon "pie-chart"}} {{i18n 'admin.dashboard.reports.no_data'}}
{{/if}} diff --git a/app/assets/stylesheets/common/admin/admin_report.scss b/app/assets/stylesheets/common/admin/admin_report.scss index 1ced78a4e6..cc98c8e14a 100644 --- a/app/assets/stylesheets/common/admin/admin_report.scss +++ b/app/assets/stylesheets/common/admin/admin_report.scss @@ -2,6 +2,15 @@ .no-data-alert { width: 100%; align-self: flex-start; + text-align: center; + padding: 1em; + + .d-icon-pie-chart { + color: $primary-low-mid; + margin-bottom: 0.5em; + font-size: $font-up-5; + display: block; + } } .conditional-loading-section { From 1708ff1808acd9e52a88a8cde4be375f94161e90 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 26 Jul 2018 15:37:56 -0400 Subject: [PATCH 006/168] UX: add a route /rules as an alias for /faq and /guidelines --- app/controllers/static_controller.rb | 6 ++-- config/routes.rb | 1 + spec/requests/static_controller_spec.rb | 39 +++++++++++-------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 715003f63b..f3aaef75f7 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -12,7 +12,9 @@ class StaticController < ApplicationController def show return redirect_to(path '/') if current_user && (params[:id] == 'login' || params[:id] == 'signup') - return redirect_to path('/login') if SiteSetting.login_required? && current_user.nil? && (params[:id] == 'faq' || params[:id] == 'guidelines') + if SiteSetting.login_required? && current_user.nil? && ['faq', 'guidelines', 'rules'].include?(params[:id]) + return redirect_to path('/login') + end map = { "faq" => { redirect: "faq_url", topic_id: "guidelines_topic_id" }, @@ -29,7 +31,7 @@ class StaticController < ApplicationController end # The /guidelines route ALWAYS shows our FAQ, ignoring the faq_url site setting. - @page = 'faq' if @page == 'guidelines' + @page = 'faq' if @page == 'guidelines' || @page == 'rules' # Don't allow paths like ".." or "/" or anything hacky like that @page.gsub!(/[^a-z0-9\_\-]/, '') diff --git a/config/routes.rb b/config/routes.rb index 3aa24380b5..a9c0aa4a1a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -328,6 +328,7 @@ Discourse::Application.routes.draw do get "password-reset" => "static#show", id: "password_reset", constraints: { format: /(json|html)/ } get "faq" => "static#show", id: "faq", constraints: { format: /(json|html)/ } get "guidelines" => "static#show", id: "guidelines", as: 'guidelines', constraints: { format: /(json|html)/ } + get "rules" => "static#show", id: "rules", as: 'rules', constraints: { format: /(json|html)/ } get "tos" => "static#show", id: "tos", as: 'tos', constraints: { format: /(json|html)/ } get "privacy" => "static#show", id: "privacy", as: 'privacy', constraints: { format: /(json|html)/ } get "signup" => "static#show", id: "signup", constraints: { format: /(json|html)/ } diff --git a/spec/requests/static_controller_spec.rb b/spec/requests/static_controller_spec.rb index 490d2966dc..8da03358d9 100644 --- a/spec/requests/static_controller_spec.rb +++ b/spec/requests/static_controller_spec.rb @@ -162,32 +162,27 @@ describe StaticController do SiteSetting.login_required = true end - it 'faq page redirects to login page for anon' do - get '/faq' - expect(response).to redirect_to '/login' + ['faq', 'guidelines', 'rules'].each do |page_name| + it "#{page_name} page redirects to login page for anon" do + get "/#{page_name}" + expect(response).to redirect_to '/login' + end + + it "#{page_name} page redirects to login page for anon" do + get "/#{page_name}" + expect(response).to redirect_to '/login' + end end - it 'guidelines page redirects to login page for anon' do - get '/guidelines' - expect(response).to redirect_to '/login' - end + ['faq', 'guidelines', 'rules'].each do |page_name| + it "#{page_name} page loads for logged in user" do + sign_in(Fabricate(:user)) - it 'faq page loads for logged in user' do - sign_in(Fabricate(:user)) + get "/#{page_name}" - get '/faq' - - expect(response.status).to eq(200) - expect(response.body).to include(I18n.t('js.faq')) - end - - it 'guidelines page loads for logged in user' do - sign_in(Fabricate(:user)) - - get '/guidelines' - - expect(response.status).to eq(200) - expect(response.body).to include(I18n.t('guidelines')) + expect(response.status).to eq(200) + expect(response.body).to include(I18n.t('guidelines')) + end end end end From c115ace27258913435fa358a1700c57cfa3de588 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 26 Jul 2018 15:45:01 -0400 Subject: [PATCH 007/168] improves danger prettier warning copy --- Dangerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dangerfile b/Dangerfile index f418ed5b2c..3941cffbed 100644 --- a/Dangerfile +++ b/Dangerfile @@ -5,7 +5,7 @@ end prettier_offenses = `prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split('\n') if !prettier_offenses.empty? fail(%{ -This PR has multiple prettier offenses. Using prettier\n +This PR doesn't match our required code formatting standards, as enforced by prettier.io. Here's how to set up prettier in your code editor.\n #{prettier_offenses.map { |o| github.html_link(o) }.join("\n")} }) end From 59684adc432553e85b6aadc70a14a18722cd11c6 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 26 Jul 2018 18:38:55 -0400 Subject: [PATCH 008/168] FIX: shows disk usage even if no backups taken (#6189) --- .../admin/controllers/admin-dashboard-next-general.js.es6 | 2 +- .../javascripts/admin/templates/dashboard_next_general.hbs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 index 42bdcc7824..878af5c4aa 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 @@ -28,7 +28,7 @@ export default Ember.Controller.extend(PeriodComputationMixin, { lastBackupTakenAt: Ember.computed.alias( "model.attributes.last_backup_taken_at" ), - shouldDisplayDurability: Ember.computed.and("lastBackupTakenAt", "diskSpace"), + shouldDisplayDurability: Ember.computed.and("diskSpace"), @computed topReferredTopicsTopions() { diff --git a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs index 71e43a2ef2..17b3c56d04 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs @@ -132,8 +132,11 @@

{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}}) -
- {{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}} + + {{#if lastBackupTakenAt}} +
+ {{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}} + {{/if}}

{{/if}} From 262beed1cf935f5777ceca0bfbbdb57e9a2b8781 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 26 Jul 2018 19:08:06 -0400 Subject: [PATCH 009/168] FIX: fixes regression with category filtering (#6190) --- app/assets/javascripts/admin/components/admin-report.js.es6 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 index 67d5b57378..a33a05f340 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -52,7 +52,7 @@ export default Ember.Component.extend({ showAllReportsLink: false, startDate: null, endDate: null, - categoryId: null, + category: null, groupId: null, showTrend: false, showHeader: true, @@ -129,6 +129,8 @@ export default Ember.Component.extend({ return displayedModesLength > 1; }, + categoryId: Ember.computed.alias("category.id"), + @computed("currentMode", "model.modes", "forcedModes") displayedModes(currentMode, reportModes, forcedModes) { const modes = forcedModes ? forcedModes.split(",") : reportModes; @@ -211,7 +213,7 @@ export default Ember.Component.extend({ actions: { refreshReport() { this.attrs.onRefresh({ - categoryId: this.get("category.id"), + categoryId: this.get("categoryId"), groupId: this.get("groupId"), startDate: this.get("startDate"), endDate: this.get("endDate") From 313cd9940d739125daf7443be2ef89c8238c34cb Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 26 Jul 2018 19:24:18 -0400 Subject: [PATCH 010/168] starts refactoring report spec --- spec/models/report_spec.rb | 170 +++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 63 deletions(-) diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 5bbdb2e1a4..4ad5c4a7ec 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -1,6 +1,37 @@ require 'rails_helper' describe Report do + shared_examples 'no data' do + context "with no data" do + it 'returns an empty report' do + expect(report.data).to be_blank + end + end + end + + shared_examples 'category filtering' do + it 'returns the filtered data' do + expect(report.total).to eq 1 + end + end + + shared_examples 'with data x/y' do + it "returns today's data" do + expect(report.data.select { |v| v[:x].today? }).to be_present + end + + it 'returns correct data for period' do + expect(report.data[0][:y]).to eq 3 + end + + it 'returns total' do + expect(report.total).to eq 4 + end + + it 'returns previous 30 day’s data' do + expect(report.prev30Days).to be_present + end + end describe "counting" do describe "requests" do @@ -60,11 +91,7 @@ describe Report do describe 'visits report' do let(:report) { Report.find('visits') } - context "no visits" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with visits" do let(:user) { Fabricate(:user) } @@ -265,11 +292,7 @@ describe Report do describe 'users by trust level report' do let(:report) { Report.find('users_by_trust_level') } - context "no users" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with users at different trust levels" do before do @@ -290,11 +313,7 @@ describe Report do describe 'new contributors report' do let(:report) { Report.find('new_contributors') } - context "no contributors" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with contributors" do before do @@ -320,11 +339,7 @@ describe Report do describe 'users by types level report' do let(:report) { Report.find('users_by_type') } - context "no users" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with users at different trust levels" do before do @@ -349,11 +364,7 @@ describe Report do describe 'trending search report' do let(:report) { Report.find('trending_search') } - context "no searches" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with different searches" do before do @@ -383,11 +394,7 @@ describe Report do describe 'DAU/MAU report' do let(:report) { Report.find('dau_by_mau') } - context "no activity" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with different users/visits" do before do @@ -420,11 +427,7 @@ describe Report do describe 'Daily engaged users' do let(:report) { Report.find('daily_engaged_users') } - context "no activity" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with different activities" do before do @@ -465,11 +468,7 @@ describe Report do describe 'flags_status' do let(:report) { Report.find('flags_status') } - context "no flags" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with flags" do let(:flagger) { Fabricate(:user) } @@ -502,11 +501,7 @@ describe Report do describe 'post_edits' do let(:report) { Report.find('post_edits') } - context "no edits" do - it "returns an empty report" do - expect(report.data).to be_blank - end - end + include_examples 'no data' context "with edits" do let(:editor) { Fabricate(:user) } @@ -535,14 +530,10 @@ describe Report do end describe 'moderator activity' do - let(:current_report) { Report.find('moderators_activity', start_date: 1.months.ago.beginning_of_day, end_date: Date.today) } + let(:report) { Report.find('moderators_activity', start_date: 1.months.ago.beginning_of_day, end_date: Date.today) } let(:previous_report) { Report.find('moderators_activity', start_date: 2.months.ago.beginning_of_day, end_date: 1.month.ago.end_of_day) } - context "no moderators" do - it "returns an empty report" do - expect(current_report.data).to be_blank - end - end + include_examples 'no data' context "with moderators" do before do @@ -585,36 +576,36 @@ describe Report do end it "returns a report with data" do - expect(current_report.data).to be_present + expect(report.data).to be_present end it "returns data for two moderators" do - expect(current_report.data.count).to eq(2) + expect(report.data.count).to eq(2) end it "returns the correct usernames" do - expect(current_report.data[0][:username]).to eq('bob') - expect(current_report.data[1][:username]).to eq('sally') + expect(report.data[0][:username]).to eq('bob') + expect(report.data[1][:username]).to eq('sally') end it "returns the correct read times" do - expect(current_report.data[0][:time_read]).to eq(300) - expect(current_report.data[1][:time_read]).to eq(3000) + expect(report.data[0][:time_read]).to eq(300) + expect(report.data[1][:time_read]).to eq(3000) end it "returns the correct agreed flag count" do - expect(current_report.data[0][:flag_count]).to be_blank - expect(current_report.data[1][:flag_count]).to eq(1) + expect(report.data[0][:flag_count]).to be_blank + expect(report.data[1][:flag_count]).to eq(1) end it "returns the correct topic count" do - expect(current_report.data[0][:topic_count]).to eq(1) - expect(current_report.data[1][:topic_count]).to be_blank + expect(report.data[0][:topic_count]).to eq(1) + expect(report.data[1][:topic_count]).to be_blank end it "returns the correct post count" do - expect(current_report.data[0][:post_count]).to be_blank - expect(current_report.data[1][:post_count]).to eq(2) + expect(report.data[0][:post_count]).to be_blank + expect(report.data[1][:post_count]).to eq(2) end it "returns the correct data for the time period" do @@ -630,4 +621,57 @@ describe Report do end end end + + describe 'flags' do + let(:report) { Report.find('flags') } + + include_examples 'no data' + + context 'with data' do + include_examples 'with data x/y' + + before(:each) do + user = Fabricate(:user) + post0 = Fabricate(:post) + post1 = Fabricate(:post, topic: Fabricate(:topic, category_id: 2)) + post2 = Fabricate(:post) + post3 = Fabricate(:post) + PostAction.act(user, post0, PostActionType.types[:off_topic]) + PostAction.act(user, post1, PostActionType.types[:off_topic]) + PostAction.act(user, post2, PostActionType.types[:off_topic]) + PostAction.act(user, post3, PostActionType.types[:off_topic]).tap do |pa| + pa.created_at = 45.days.ago + end.save + end + + context "with category filtering" do + let(:report) { Report.find('flags', category_id: 2) } + + include_examples 'category filtering' + end + end + end + + describe 'topics' do + let(:report) { Report.find('topics') } + + include_examples 'no data' + + context 'with data' do + include_examples 'with data x/y' + + before(:each) do + Fabricate(:topic) + Fabricate(:topic, category_id: 2) + Fabricate(:topic) + Fabricate(:topic, created_at: 45.days.ago) + end + + context "with category filtering" do + let(:report) { Report.find('topics', category_id: 2) } + + include_examples 'category filtering' + end + end + end end From ccf76d45f21a85b5f9d244e8b0e2a0986b2d7491 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 27 Jul 2018 08:19:11 +0800 Subject: [PATCH 011/168] FIX: Missing variable outside of `begin` block. --- app/jobs/regular/emit_web_hook_event.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 742c3b7f2b..5cd4010abc 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -70,6 +70,7 @@ module Jobs body = build_web_hook_body(args, web_hook) web_hook_event = WebHookEvent.create!(web_hook_id: web_hook.id) + response = nil begin content_type = @@ -118,7 +119,7 @@ module Jobs web_hook_event.destroy! end - retry_web_hook if response.status != 200 + retry_web_hook if response&.status != 200 end def retry_web_hook From 5e262265a2f9b85c110bfc039ed65d093bce022c Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 27 Jul 2018 12:51:06 +1000 Subject: [PATCH 012/168] update script to provide more mem stats --- script/mwrap_sidekiq | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/script/mwrap_sidekiq b/script/mwrap_sidekiq index 318e483366..1249a3c8d8 100755 --- a/script/mwrap_sidekiq +++ b/script/mwrap_sidekiq @@ -1,6 +1,9 @@ #!/usr/bin/env ruby if !ENV["LD_PRELOAD"]&.include?('mwrap') + # use malloc from libc that interacts better with mwrap ENV['RAILS_ENV'] = 'production' + ENV["LD_PRELOAD"] = "" + ENV["MALLOC_ARENA_MAX"] = "2" exec "mwrap #{__FILE__}" end @@ -47,11 +50,16 @@ def render_table(array) buffer end +def rss + `ps -o rss= -p #{Process.pid}`.chomp.to_i +end + def mwrap_log report = +"" Mwrap.quiet do - report << "Generation: #{GC.count}\n\n" + report << "Generation: #{GC.count} RSS kb: #{rss} Accounted Mem kb: #{(Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed) / 1024}\n" + report << "Allocated bytes: #{Mwrap.total_bytes_allocated} Freed bytes: #{Mwrap.total_bytes_freed}\n\n" table = [] Mwrap.each(200) do |loc, total, allocations, frees, age_sum, max_life| From 6740631fdb379f764e8d3007447341f4496dc350 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 27 Jul 2018 12:48:16 +0800 Subject: [PATCH 013/168] TEMPFIX: Fix broken restores. --- lib/backup_restore/restorer.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index fefe6c4577..f3b7ffbe5a 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -64,6 +64,21 @@ module BackupRestore wait_for_sidekiq BackupRestore.move_tables_between_schemas("public", "backup") + + # This is a temp fix to allow restores to work again. @tgxworld is + # current working on a fix that namespaces functions created by Discourse + # so that we can alter the schema of those functions before restoring. + %w{ + raise_email_logs_reply_key_readonly + raise_email_logs_skipped_reason_readonly + }.each do |function| + DB.exec(<<~SQL) + DROP FUNCTION IF EXISTS backup.#{function}; + ALTER FUNCTION public.#{function} + SET SCHEMA "backup"; + SQL + end + @db_was_changed = true restore_dump migrate_database From b7d1864d0a6e1f249d572c1e0bf088d1f24c32b7 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 27 Jul 2018 01:22:00 -0400 Subject: [PATCH 014/168] FIX: simplify filters on admin-report component (#6193) --- .../admin/components/admin-report.js.es6 | 3 ++- .../admin-dashboard-next-general.js.es6 | 7 ++++- .../admin-dashboard-next-moderation.js.es6 | 10 +++++++ .../controllers/admin-reports-show.js.es6 | 2 +- .../templates/dashboard_next_general.hbs | 26 ++++++------------- .../templates/dashboard_next_moderation.hbs | 9 +++---- .../admin/templates/reports-show.hbs | 2 +- 7 files changed, 31 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 index a33a05f340..74c96f814a 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -50,6 +50,7 @@ export default Ember.Component.extend({ reportOptions: null, forcedModes: null, showAllReportsLink: false, + filters: null, startDate: null, endDate: null, category: null, @@ -77,7 +78,7 @@ export default Ember.Component.extend({ didReceiveAttrs() { this._super(...arguments); - const state = this.get("filteringState") || {}; + const state = this.get("filters") || {}; this.setProperties({ category: Category.findById(state.categoryId), groupId: state.groupId, diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 index 878af5c4aa..b8b7473967 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 @@ -77,6 +77,11 @@ export default Ember.Controller.extend(PeriodComputationMixin, { } }, + @computed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + @computed("model.attributes.updated_at") updatedTimestamp(updatedAt) { return moment(updatedAt).format("LLL"); @@ -88,6 +93,6 @@ export default Ember.Controller.extend(PeriodComputationMixin, { }, _reportsForPeriodURL(period) { - return Discourse.getURL(`/admin/dashboard/general?period=${period}`); + return Discourse.getURL(`/admin?period=${period}`); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 index afe81cbf51..059bcd6176 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 @@ -12,6 +12,16 @@ export default Ember.Controller.extend(PeriodComputationMixin, { }; }, + @computed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + + @computed("lastWeek", "endDate") + lastWeekfilters(startDate, endDate) { + return { startDate, endDate }; + }, + _reportsForPeriodURL(period) { return Discourse.getURL(`/admin/dashboard/moderation?period=${period}`); } diff --git a/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 index fdabfd896e..c86bbb60ec 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 @@ -15,7 +15,7 @@ export default Ember.Controller.extend({ }, @computed("category_id", "group_id", "start_date", "end_date") - filteringState(categoryId, groupId, startDate, endDate) { + filters(categoryId, groupId, startDate, endDate) { return { categoryId, groupId, diff --git a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs index 17b3c56d04..0d03c548aa 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs @@ -18,43 +18,37 @@ dataSourceName="signups" showTrend=true forcedModes="chart" - startDate=startDate - endDate=endDate}} + filters=filters}} {{admin-report dataSourceName="topics" showTrend=true forcedModes="chart" - startDate=startDate - endDate=endDate}} + filters=filters}} {{admin-report dataSourceName="posts" showTrend=true forcedModes="chart" - startDate=startDate - endDate=endDate}} + filters=filters}} {{admin-report dataSourceName="dau_by_mau" showTrend=true forcedModes="chart" - startDate=startDate - endDate=endDate}} + filters=filters}} {{admin-report dataSourceName="daily_engaged_users" showTrend=true forcedModes="chart" - startDate=startDate - endDate=endDate}} + filters=filters}} {{admin-report dataSourceName="new_contributors" showTrend=true forcedModes="chart" - startDate=startDate - endDate=endDate}} + filters=filters}} @@ -170,17 +164,13 @@
{{admin-report dataSourceName="top_referred_topics" - reportOptions=topReferredTopicsTopions - startDate=startDate - endDate=endDate}} + reportOptions=topReferredTopicsTopions}} {{admin-report dataSourceName="trending_search" reportOptions=trendingSearchOptions isEnabled=logSearchQueriesEnabled - disabledLabel="admin.dashboard.reports.trending_search.disabled" - startDate=startDate - endDate=endDate}} + disabledLabel="admin.dashboard.reports.trending_search.disabled"}} {{{i18n "admin.dashboard.reports.trending_search.more"}}}
diff --git a/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs b/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs index 133519fb98..5a5eb153e3 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs @@ -17,8 +17,7 @@
{{admin-report - startDate=startDate - endDate=endDate + filters=filters showHeader=false dataSourceName="moderators_activity"}}
@@ -27,14 +26,12 @@
{{admin-report dataSourceName="flags_status" - startDate=lastWeek reportOptions=flagsStatusOptions - endDate=endDate}} + filters=lastWeekfilters}} {{admin-report dataSourceName="post_edits" - startDate=lastWeek - endDate=endDate}} + filters=lastWeekfilters}} {{plugin-outlet name="admin-dashboard-moderation-bottom"}}
diff --git a/app/assets/javascripts/admin/templates/reports-show.hbs b/app/assets/javascripts/admin/templates/reports-show.hbs index 8649abe5db..d595ff46e9 100644 --- a/app/assets/javascripts/admin/templates/reports-show.hbs +++ b/app/assets/javascripts/admin/templates/reports-show.hbs @@ -3,7 +3,7 @@ {{admin-report showAllReportsLink=true dataSourceName=model.type - filteringState=filteringState + filters=filters reportOptions=reportOptions showFilteringUI=true onRefresh=(action "onParamsChange")}} From e4208113a8bd70a2aa1a6c3ff365a95fd5f05476 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 27 Jul 2018 16:21:58 +1000 Subject: [PATCH 015/168] improve report and add regular logging --- script/mwrap_sidekiq | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/script/mwrap_sidekiq b/script/mwrap_sidekiq index 1249a3c8d8..ca5c139ac2 100755 --- a/script/mwrap_sidekiq +++ b/script/mwrap_sidekiq @@ -59,7 +59,12 @@ def mwrap_log Mwrap.quiet do report << "Generation: #{GC.count} RSS kb: #{rss} Accounted Mem kb: #{(Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed) / 1024}\n" - report << "Allocated bytes: #{Mwrap.total_bytes_allocated} Freed bytes: #{Mwrap.total_bytes_freed}\n\n" + report << "Allocated bytes: #{Mwrap.total_bytes_allocated} Freed bytes: #{Mwrap.total_bytes_freed}\n" + stat = GC.stat + stat.each do |k, v| + report << "#{k}: #{v}\n" + end + report << "\n" table = [] Mwrap.each(200) do |loc, total, allocations, frees, age_sum, max_life| @@ -76,6 +81,25 @@ def mwrap_log report end +Thread.new do + begin + puts "Starting Logging Thread" + path = "/tmp/mwrap_#{Process.pid}" + `mkdir -p #{path}` + + while true + log = mwrap_log + f = "#{path}/log_#{Time.now.strftime("%Y_%m_%d_%H%M%S")}" + File.write(f, log) + puts "Wrote #{f}" + sleep 60 * 60 + end + rescue => e + STDERR.puts "ERROR crashed logger #{e}" + STDERR.puts e.backtrace + end +end + Thread.new do puts "Starting WEBrick on port 9874" server = WEBrick::HTTPServer.new(Port: 9874) From dac29b5ebc5d10bce754baeca88d97cb045b420c Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 27 Jul 2018 12:11:07 +0530 Subject: [PATCH 016/168] UX: Display only top categories in hamburger menu (#6146) --- .../widgets/hamburger-categories.js.es6 | 19 +++++- .../discourse/widgets/hamburger-menu.js.es6 | 33 +++++++---- .../stylesheets/common/base/menu-panel.scss | 9 +++ app/serializers/current_user_serializer.rb | 20 ++++++- config/locales/client.en.yml | 1 + .../current_user_serializer_spec.rb | 27 +++++++++ .../widgets/hamburger-menu-test.js.es6 | 59 ++++++++++--------- 7 files changed, 122 insertions(+), 46 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 index 56ffa45a99..af0c7be0ba 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 @@ -51,7 +51,7 @@ export default createWidget("hamburger-categories", { html(attrs) { const href = Discourse.getURL("/categories"); - const result = [ + let result = [ h( "li.heading", h( @@ -66,8 +66,23 @@ export default createWidget("hamburger-categories", { if (categories.length === 0) { return; } - return result.concat( + result = result.concat( categories.map(c => this.attach("hamburger-category", c)) ); + + if (attrs.showMore) { + result = result.concat( + h( + "li.footer", + h( + "a.d-link.more-link", + { attributes: { href } }, + I18n.t("categories.more") + ) + ) + ); + } + + return result; } }); diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 66ef51a080..ca141b5fd4 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -176,20 +176,29 @@ export default createWidget("hamburger-menu", { }, listCategories() { - const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; - const isStaff = Discourse.User.currentProp("staff"); + const maxCategoriesToDisplay = 6; + const categoriesList = this.site + .get("categoriesByCount") + .reject(c => c.parent_category_id); + let categories = []; + let showMore = categoriesList.length > maxCategoriesToDisplay; - const categories = this.site.get("categoriesList").reject(c => { - if (c.get("parentCategory.show_subcategory_list")) { - return true; - } - if (hideUncategorized && c.get("isUncategorizedCategory") && !isStaff) { - return true; - } - return false; - }); + if (this.currentUser) { + let categoryIds = this.currentUser.get("top_category_ids") || []; + categoryIds = categoryIds.concat(categoriesList.map(c => c.id)).uniq(); - return this.attach("hamburger-categories", { categories }); + showMore = categoryIds.length > maxCategoriesToDisplay; + categoryIds = categoryIds.slice(0, maxCategoriesToDisplay); + + categories = categoryIds.map(id => { + return categoriesList.find(c => c.id === id); + }); + } else { + showMore = categoriesList.length > maxCategoriesToDisplay; + categories = categoriesList.slice(0, maxCategoriesToDisplay); + } + + return this.attach("hamburger-categories", { categories, showMore }); }, footerLinks(prioritizeFaq, faqUrl) { diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 18c5083e8d..9410596337 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -108,6 +108,15 @@ } } + .category-links { + .footer { + clear: both; + display: block; + text-align: right; + padding-right: 12px; + } + } + // note these topic counts only appear for anons in the category hamburger drop down b.topics-count { color: dark-light-choose($primary-medium, $secondary-medium); diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 58a078c68d..a2fffd2187 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -2,6 +2,8 @@ require_dependency 'new_post_manager' class CurrentUserSerializer < BasicUserSerializer + MAX_TOP_CATEGORIES_COUNT = 6.freeze + attributes :name, :unread_notifications, :unread_private_messages, @@ -41,7 +43,8 @@ class CurrentUserSerializer < BasicUserSerializer :primary_group_name, :can_create_topic, :link_posting_access, - :external_id + :external_id, + :top_category_ids def link_posting_access scope.link_posting_access @@ -153,9 +156,20 @@ class CurrentUserSerializer < BasicUserSerializer end def muted_category_ids - @muted_category_ids ||= CategoryUser.where(user_id: object.id, - notification_level: TopicUser.notification_levels[:muted]) + CategoryUser.lookup(object, :muted).pluck(:category_id) + end + + def top_category_ids + CategoryUser.where(user_id: object.id) + .where.not(notification_level: CategoryUser.notification_levels[:muted]) + .order(" + CASE + WHEN notification_level = 3 THEN 1 + WHEN notification_level = 2 THEN 2 + WHEN notification_level = 4 THEN 3 + END") .pluck(:category_id) + .slice(0, MAX_TOP_CATEGORIES_COUNT) end def dismissed_banner_key diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b42fbfd170..1d36c4c642 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -571,6 +571,7 @@ en: topic_stat_sentence: one: "%{count} new topic in the past %{unit}." other: "%{count} new topics in the past %{unit}." + more: "more" ip_lookup: title: IP Address Lookup diff --git a/spec/serializers/current_user_serializer_spec.rb b/spec/serializers/current_user_serializer_spec.rb index 5e11505bb0..46ca334f71 100644 --- a/spec/serializers/current_user_serializer_spec.rb +++ b/spec/serializers/current_user_serializer_spec.rb @@ -32,4 +32,31 @@ RSpec.describe CurrentUserSerializer do expect(payload[:external_id]).to eq("12345") end end + + context "#top_category_ids" do + let(:user) { Fabricate(:user) } + let(:category1) { Fabricate(:category) } + let(:category2) { Fabricate(:category) } + let :serializer do + CurrentUserSerializer.new(user, scope: Guardian.new, root: false) + end + + it "should include empty top_category_ids array" do + payload = serializer.as_json + expect(payload[:top_category_ids]).to eq([]) + end + + it "should include correct id in top_category_ids array" do + category = Category.first + CategoryUser.create!(user_id: user.id, + category_id: category1.id, + notification_level: CategoryUser.notification_levels[:tracking]) + + CategoryUser.create!(user_id: user.id, + category_id: category2.id, + notification_level: CategoryUser.notification_levels[:watching]) + payload = serializer.as_json + expect(payload[:top_category_ids]).to eq([category2.id, category1.id]) + end + end end diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 index 1dbdfcca6c..04d30f5369 100644 --- a/test/javascripts/widgets/hamburger-menu-test.js.es6 +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -2,6 +2,9 @@ import { moduleForWidget, widgetTest } from "helpers/widget-test"; moduleForWidget("hamburger-menu"); +const maxCategoriesToDisplay = 6; +const topCategoryIds = [2, 3, 1]; + widgetTest("prioritize faq", { template: '{{mount-widget widget="hamburger-menu"}}', @@ -125,42 +128,40 @@ widgetTest("general links", { } }); -widgetTest("category links", { +widgetTest("top categories - anonymous", { template: '{{mount-widget widget="hamburger-menu"}}', anonymous: true, + test(assert) { + const count = this.site.get("categoriesByCount").length; + const maximum = + count <= maxCategoriesToDisplay ? count : maxCategoriesToDisplay; + assert.equal(find(".category-link").length, maximum); + } +}); + +widgetTest("top categories", { + template: '{{mount-widget widget="hamburger-menu"}}', + beforeEach() { - const cat = this.site.get("categoriesList")[0]; - - const parent = Discourse.Category.create({ - id: 1, - topic_count: 5, - name: "parent", - url: "https://test.com/parent", - show_subcategory_list: true, - topicTrackingState: cat.get("topicTrackingState") - }); - const child = Discourse.Category.create({ - id: 2, - parent_category_id: 1, - parentCategory: parent, - topic_count: 4, - name: "child", - url: "https://test.com/child", - topicTrackingState: cat.get("topicTrackingState") - }); - - parent.subcategories = [child]; - - const list = [parent, child]; - this.site.set("categoriesList", list); + this.currentUser.set("top_category_ids", topCategoryIds); }, test(assert) { - // if show_subcategory_list is enabled we suppress the categories from hamburger - // this means that people can be confused about counts - assert.equal(this.$(".category-link").length, 1); - assert.equal(this.$(".category-link .topics-count").text(), "9"); + assert.equal(find(".category-link").length, maxCategoriesToDisplay); + + const categoriesList = this.site + .get("categoriesByCount") + .reject(c => c.parent_category_id); + let ids = topCategoryIds + .concat(categoriesList.map(c => c.id)) + .uniq() + .slice(0, maxCategoriesToDisplay); + + assert.equal( + find(".category-link .category-name").text(), + ids.map(i => categoriesList.find(c => c.id === i).name).join("") + ); } }); From c0f7d95bb83394f4ab04e445dd96a6f8ffc209d9 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 27 Jul 2018 13:05:18 +0530 Subject: [PATCH 017/168] Include subcategories in hamburger menu top categories --- app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index ca141b5fd4..28ed247242 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -178,8 +178,7 @@ export default createWidget("hamburger-menu", { listCategories() { const maxCategoriesToDisplay = 6; const categoriesList = this.site - .get("categoriesByCount") - .reject(c => c.parent_category_id); + .get("categoriesByCount"); let categories = []; let showMore = categoriesList.length > maxCategoriesToDisplay; From 5b4e13bfcccf35181542435f2090481d3a5cd29e Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 27 Jul 2018 13:20:21 +0530 Subject: [PATCH 018/168] Make prettier happy --- app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 28ed247242..dc0471fc1f 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -177,8 +177,7 @@ export default createWidget("hamburger-menu", { listCategories() { const maxCategoriesToDisplay = 6; - const categoriesList = this.site - .get("categoriesByCount"); + const categoriesList = this.site.get("categoriesByCount"); let categories = []; let showMore = categoriesList.length > maxCategoriesToDisplay; From c74dd2fa0848ac527c4715edb7f50114e00681de Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 27 Jul 2018 15:14:23 +0530 Subject: [PATCH 019/168] FIX: welcome topic should not be a private message --- lib/introduction_updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/introduction_updater.rb b/lib/introduction_updater.rb index a576b0433c..4868e7f22b 100644 --- a/lib/introduction_updater.rb +++ b/lib/introduction_updater.rb @@ -30,7 +30,7 @@ protected end def find_welcome_post - welcome_topic = Topic.where(slug: 'welcome-to-discourse').first + welcome_topic = Topic.listable_topics.where(slug: 'welcome-to-discourse').first return nil unless welcome_topic.present? post = welcome_topic.posts.where(post_number: 1).first From 9c72c00206c3e9cb9c3076f2d4a297e6296e7e74 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 27 Jul 2018 12:28:51 +0100 Subject: [PATCH 020/168] FEATURE: Revoke and reconnect for Twitter logins --- .../discourse/models/login-method.js.es6 | 2 +- lib/auth/twitter_authenticator.rb | 37 +++++++++++++++-- .../auth/twitter_authenticator_spec.rb | 41 +++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 index 515594b435..ef291594ea 100644 --- a/app/assets/javascripts/discourse/models/login-method.js.es6 +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -127,7 +127,7 @@ export function findAll(siteSettings, capabilities, isMobileDevice) { params.displayPopup = true; } - if (["facebook", "google_oauth2"].includes(name)) { + if (["facebook", "google_oauth2", "twitter"].includes(name)) { params.canConnect = true; } diff --git a/lib/auth/twitter_authenticator.rb b/lib/auth/twitter_authenticator.rb index c6ffbe2145..be591a0235 100644 --- a/lib/auth/twitter_authenticator.rb +++ b/lib/auth/twitter_authenticator.rb @@ -13,7 +13,26 @@ class Auth::TwitterAuthenticator < Auth::Authenticator info&.email || info&.screen_name || "" end - def after_authenticate(auth_token) + def can_revoke? + true + end + + def revoke(user, skip_remote: false) + info = TwitterUserInfo.find_by(user_id: user.id) + raise Discourse::NotFound if info.nil? + + # We get a token from twitter upon login but do not need it, and do not store it. + # Therefore we do not have any way to revoke the token automatically on twitter's end + + info.destroy! + true + end + + def can_connect_existing_user? + true + end + + def after_authenticate(auth_token, existing_account: nil) result = Auth::Result.new data = auth_token[:info] @@ -35,9 +54,21 @@ class Auth::TwitterAuthenticator < Auth::Authenticator user_info = TwitterUserInfo.find_by(twitter_user_id: twitter_user_id) - result.user = user_info.try(:user) + if existing_account && (user_info.nil? || existing_account.id != user_info.user_id) + user_info.destroy! if user_info + result.user = existing_account + user_info = TwitterUserInfo.create!( + user_id: result.user.id, + screen_name: result.username, + twitter_user_id: twitter_user_id, + email: result.email + ) + else + result.user = user_info&.user + end + if (!result.user) && result.email_valid && (result.user = User.find_by_email(result.email)) - TwitterUserInfo.create( + TwitterUserInfo.create!( user_id: result.user.id, screen_name: result.username, twitter_user_id: twitter_user_id, diff --git a/spec/components/auth/twitter_authenticator_spec.rb b/spec/components/auth/twitter_authenticator_spec.rb index 8c2d465965..f6b620dfdf 100644 --- a/spec/components/auth/twitter_authenticator_spec.rb +++ b/spec/components/auth/twitter_authenticator_spec.rb @@ -25,4 +25,45 @@ describe Auth::TwitterAuthenticator do expect(info.email).to eq(user.email) end + it 'can connect to a different existing user account' do + authenticator = Auth::TwitterAuthenticator.new + user1 = Fabricate(:user) + user2 = Fabricate(:user) + + TwitterUserInfo.create!(user_id: user1.id, twitter_user_id: 100, screen_name: "boris") + + hash = { + info: { + "email" => user1.email, + "username" => "test", + "name" => "test", + "nickname" => "minion", + }, + "uid" => "100" + } + + result = authenticator.after_authenticate(hash, existing_account: user2) + + expect(result.user.id).to eq(user2.id) + expect(TwitterUserInfo.exists?(user_id: user1.id)).to eq(false) + expect(TwitterUserInfo.exists?(user_id: user2.id)).to eq(true) + end + + context 'revoke' do + let(:user) { Fabricate(:user) } + let(:authenticator) { Auth::TwitterAuthenticator.new } + + it 'raises exception if no entry for user' do + expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) + end + + it 'revokes correctly' do + TwitterUserInfo.create!(user_id: user.id, twitter_user_id: 100, screen_name: "boris") + expect(authenticator.can_revoke?).to eq(true) + expect(authenticator.revoke(user)).to eq(true) + expect(authenticator.description_for_user(user)).to eq("") + end + + end + end From 85291e53f18d90ff955ae7223d8edb637f8e1113 Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Fri, 27 Jul 2018 22:17:29 +0800 Subject: [PATCH 021/168] UX: more categories link alignment --- app/assets/stylesheets/common/base/menu-panel.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 9410596337..0d11ebb4f8 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -112,8 +112,7 @@ .footer { clear: both; display: block; - text-align: right; - padding-right: 12px; + padding: 0.25em 0.5em; } } From bc501038cb5195048907c6e14cf319aa74ff4e07 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 27 Jul 2018 20:00:39 +0530 Subject: [PATCH 022/168] FIX: Remove null value from categories list --- app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index dc0471fc1f..50e00467f5 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -190,7 +190,7 @@ export default createWidget("hamburger-menu", { categories = categoryIds.map(id => { return categoriesList.find(c => c.id === id); - }); + }).filter(c => c); } else { showMore = categoriesList.length > maxCategoriesToDisplay; categories = categoriesList.slice(0, maxCategoriesToDisplay); From a9c959e3e2179577053fb8af8ff8664a5b655942 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 27 Jul 2018 20:39:44 +0530 Subject: [PATCH 023/168] Make prettier happy --- app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 50e00467f5..ef84b58788 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -190,7 +190,8 @@ export default createWidget("hamburger-menu", { categories = categoryIds.map(id => { return categoriesList.find(c => c.id === id); - }).filter(c => c); + }); + categories = categories.filter(c => c); } else { showMore = categoriesList.length > maxCategoriesToDisplay; categories = categoriesList.slice(0, maxCategoriesToDisplay); From 6296f63804cbf8ae6d559cf66d6be1da0f934bd1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 27 Jul 2018 16:20:47 +0100 Subject: [PATCH 024/168] FEATURE: Revoke and connect for Yahoo logins --- .../discourse/models/login-method.js.es6 | 2 +- lib/auth/open_id_authenticator.rb | 23 +++++++++++- .../auth/open_id_authenticator_spec.rb | 35 +++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 index ef291594ea..6fab5b0577 100644 --- a/app/assets/javascripts/discourse/models/login-method.js.es6 +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -127,7 +127,7 @@ export function findAll(siteSettings, capabilities, isMobileDevice) { params.displayPopup = true; } - if (["facebook", "google_oauth2", "twitter"].includes(name)) { + if (["facebook", "google_oauth2", "twitter", "yahoo"].includes(name)) { params.canConnect = true; } diff --git a/lib/auth/open_id_authenticator.rb b/lib/auth/open_id_authenticator.rb index f4cfbd011e..849ca6977a 100644 --- a/lib/auth/open_id_authenticator.rb +++ b/lib/auth/open_id_authenticator.rb @@ -18,7 +18,23 @@ class Auth::OpenIdAuthenticator < Auth::Authenticator info&.email || "" end - def after_authenticate(auth_token) + def can_revoke? + true + end + + def revoke(user, skip_remote: false) + info = UserOpenId.where("url LIKE ?", "#{@identifier}%").find_by(user_id: user.id) + raise Discourse::NotFound if info.nil? + + info.destroy! + true + end + + def can_connect_existing_user? + true + end + + def after_authenticate(auth_token, existing_account: nil) result = Auth::Result.new data = auth_token[:info] @@ -33,6 +49,11 @@ class Auth::OpenIdAuthenticator < Auth::Authenticator user_open_id = UserOpenId.find_by_url(identity_url) + if existing_account && (user_open_id.nil? || existing_account.id != user_open_id.user_id) + user_open_id.destroy! if user_open_id + user_open_id = UserOpenId.create!(url: identity_url , user_id: existing_account.id, email: email, active: true) + end + if !user_open_id && @opts[:trusted] && user = User.find_by_email(email) user_open_id = UserOpenId.create(url: identity_url , user_id: user.id, email: email, active: true) end diff --git a/spec/components/auth/open_id_authenticator_spec.rb b/spec/components/auth/open_id_authenticator_spec.rb index 0ae354ebb9..761bd1af95 100644 --- a/spec/components/auth/open_id_authenticator_spec.rb +++ b/spec/components/auth/open_id_authenticator_spec.rb @@ -22,4 +22,39 @@ describe Auth::OpenIdAuthenticator do response = OpenStruct.new(identity_url: 'abc') expect { auth.after_authenticate(info: {}, extra: { response: response }) }.to raise_error(Discourse::InvalidParameters) end + + it 'can connect to a different existing user account' do + authenticator = Auth::OpenIdAuthenticator.new("test", "id", "enable_yahoo_logins", trusted: true) + user1 = Fabricate(:user) + user2 = Fabricate(:user) + + UserOpenId.create!(url: "id/123" , user_id: user1.id, email: "bob@example.com", active: true) + + hash = { + info: { email: user1.email }, extra: { response: OpenStruct.new(identity_url: 'id/123') } + } + + result = authenticator.after_authenticate(hash, existing_account: user2) + + expect(result.user.id).to eq(user2.id) + expect(UserOpenId.exists?(user_id: user1.id)).to eq(false) + expect(UserOpenId.exists?(user_id: user2.id)).to eq(true) + end + + context 'revoke' do + let(:user) { Fabricate(:user) } + let(:authenticator) { Auth::OpenIdAuthenticator.new("test", "id", "enable_yahoo_logins", trusted: true) } + + it 'raises exception if no entry for user' do + expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) + end + + it 'revokes correctly' do + UserOpenId.create!(url: "id/123" , user_id: user.id, email: "bob@example.com", active: true) + expect(authenticator.can_revoke?).to eq(true) + expect(authenticator.revoke(user)).to eq(true) + expect(authenticator.description_for_user(user)).to eq("") + end + + end end From 5f1fd0019bf2fb7f52037b223e1287a7d467c66b Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 27 Jul 2018 17:18:53 +0100 Subject: [PATCH 025/168] FEATURE: Allow revoke and connect for GitHub logins --- .../discourse/models/login-method.js.es6 | 6 +++- lib/auth/github_authenticator.rb | 22 +++++++++++++- .../auth/github_authenticator_spec.rb | 30 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 index 6fab5b0577..5105179104 100644 --- a/app/assets/javascripts/discourse/models/login-method.js.es6 +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -127,7 +127,11 @@ export function findAll(siteSettings, capabilities, isMobileDevice) { params.displayPopup = true; } - if (["facebook", "google_oauth2", "twitter", "yahoo"].includes(name)) { + if ( + ["facebook", "google_oauth2", "twitter", "yahoo", "github"].includes( + name + ) + ) { params.canConnect = true; } diff --git a/lib/auth/github_authenticator.rb b/lib/auth/github_authenticator.rb index e5ac4ee30e..aaf43fa1b2 100644 --- a/lib/auth/github_authenticator.rb +++ b/lib/auth/github_authenticator.rb @@ -15,6 +15,17 @@ class Auth::GithubAuthenticator < Auth::Authenticator info&.screen_name || "" end + def can_revoke? + true + end + + def revoke(user, skip_remote: false) + info = GithubUserInfo.find_by(user_id: user.id) + raise Discourse::NotFound if info.nil? + info.destroy! + true + end + class GithubEmailChecker include ::HasErrors @@ -30,7 +41,7 @@ class Auth::GithubAuthenticator < Auth::Authenticator end - def after_authenticate(auth_token) + def after_authenticate(auth_token, existing_account: nil) result = Auth::Result.new data = auth_token[:info] @@ -46,6 +57,15 @@ class Auth::GithubAuthenticator < Auth::Authenticator user_info = GithubUserInfo.find_by(github_user_id: github_user_id) + if existing_account && (user_info.nil? || existing_account.id != user_info.user_id) + user_info.destroy! if user_info + user_info = GithubUserInfo.create( + user_id: existing_account.id, + screen_name: screen_name, + github_user_id: github_user_id + ) + end + if user_info # If there's existing user info with the given GitHub ID, that's all we # need to know. diff --git a/spec/components/auth/github_authenticator_spec.rb b/spec/components/auth/github_authenticator_spec.rb index 17fff987cd..4ade566557 100644 --- a/spec/components/auth/github_authenticator_spec.rb +++ b/spec/components/auth/github_authenticator_spec.rb @@ -202,6 +202,36 @@ describe Auth::GithubAuthenticator do expect(result.email_valid).to eq(true) end + it 'can connect to a different existing user account' do + user1 = Fabricate(:user) + user2 = Fabricate(:user) + + GithubUserInfo.create!(user_id: user1.id, github_user_id: 100, screen_name: "boris") + + result = authenticator.after_authenticate(data, existing_account: user2) + + expect(result.user.id).to eq(user2.id) + expect(GithubUserInfo.exists?(user_id: user1.id)).to eq(false) + expect(GithubUserInfo.exists?(user_id: user2.id)).to eq(true) + end + + end + + context 'revoke' do + let(:user) { Fabricate(:user) } + let(:authenticator) { Auth::GithubAuthenticator.new } + + it 'raises exception if no entry for user' do + expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) + end + + it 'revokes correctly' do + GithubUserInfo.create!(user_id: user.id, github_user_id: 100, screen_name: "boris") + expect(authenticator.can_revoke?).to eq(true) + expect(authenticator.revoke(user)).to eq(true) + expect(authenticator.description_for_user(user)).to eq("") + end + end describe 'avatar retrieval' do From 60b2c3207e40a7b5a244d0e0705c9192e60dcc04 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 27 Jul 2018 13:10:13 -0400 Subject: [PATCH 026/168] larger mobile topic list badges --- app/assets/stylesheets/mobile/topic-list.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 484a86df3e..d9c3f14e12 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -152,9 +152,9 @@ position: relative; display: inline-block; top: -1px; - font-size: $font-down-1; + font-size: $font-0; line-height: $line-height-small; - padding: 0.25em 0.45em; + padding: 0.15em 0.4em 0.2em 0.4em; i { color: $secondary; } @@ -482,7 +482,7 @@ button.dismiss-read { // base defines extra padding for easier click/top of title field // this is a bit too much for mobile td .main-link { - width: 79%; + width: 78%; display: inline-block; a.title { padding: 5px 10px 5px 0; From 3159c46613e0a55462c73ab85d294b0ba2a0b310 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 27 Jul 2018 14:55:51 -0400 Subject: [PATCH 027/168] Making dataless sections stand out less --- app/assets/stylesheets/common/admin/admin_report.scss | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/common/admin/admin_report.scss b/app/assets/stylesheets/common/admin/admin_report.scss index cc98c8e14a..cab0afe45d 100644 --- a/app/assets/stylesheets/common/admin/admin_report.scss +++ b/app/assets/stylesheets/common/admin/admin_report.scss @@ -1,13 +1,16 @@ .admin-report { .no-data-alert { + background: $secondary; + border: 1px solid $primary-low; + color: $primary-low-mid; width: 100%; align-self: flex-start; text-align: center; - padding: 1em; + padding: 3em; .d-icon-pie-chart { - color: $primary-low-mid; - margin-bottom: 0.5em; + color: currentColor; + margin-bottom: 0.25em; font-size: $font-up-5; display: block; } From a8f1b0768602e8f9528e1ead39dc27c34c18306e Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 27 Jul 2018 16:29:30 -0400 Subject: [PATCH 028/168] FIX: prevents exception when loading old dashboard (#6196) --- app/assets/javascripts/admin/models/admin-dashboard.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 index 75757656d6..9ce5a79e15 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 @@ -11,7 +11,7 @@ AdminDashboard.reopenClass({ @return {jqXHR} a jQuery Promise object **/ find: function() { - return ajax("/admin/dashboard.json").then(function(json) { + return ajax("/admin/dashboard-old.json").then(function(json) { var model = AdminDashboard.create(json); model.set("loaded", true); return model; From 7a1a1eb4c0eb32922899b9b8732c1c8f77e8663c Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 27 Jul 2018 16:49:28 -0400 Subject: [PATCH 029/168] improve header scaling with font size --- app/assets/stylesheets/common/base/header.scss | 2 +- app/assets/stylesheets/desktop/header.scss | 2 +- app/assets/stylesheets/mobile/header.scss | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 07e6c57873..ec9313d4dc 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -24,7 +24,7 @@ } #site-logo { - max-height: 40px; + max-height: 2.8571em; } .d-icon-home { diff --git a/app/assets/stylesheets/desktop/header.scss b/app/assets/stylesheets/desktop/header.scss index 4bdac207a7..e4b20e264f 100644 --- a/app/assets/stylesheets/desktop/header.scss +++ b/app/assets/stylesheets/desktop/header.scss @@ -28,7 +28,7 @@ } #main-outlet { - padding-top: 82px; + padding-top: 5.8572em; } .search-link .blurb { diff --git a/app/assets/stylesheets/mobile/header.scss b/app/assets/stylesheets/mobile/header.scss index d77d3859b3..3a7ecb8797 100644 --- a/app/assets/stylesheets/mobile/header.scss +++ b/app/assets/stylesheets/mobile/header.scss @@ -13,7 +13,7 @@ .d-header { #site-logo { - max-width: 130px; + max-width: 9.2857em; } // some protection for text-only site titles @@ -47,7 +47,7 @@ } #main-outlet { - padding-top: 60px; + padding-top: 4.2857em; } .search-link .badge-category { From 3dbaaf0d74f556e536bdc0bfb9b14956c0df8fc9 Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Sat, 28 Jul 2018 11:51:53 +0800 Subject: [PATCH 030/168] UX: gives wizard language selector more room to expand --- app/assets/stylesheets/wizard.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index e8564a25c5..7b4526453a 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -75,7 +75,6 @@ body.wizard { .wizard-step-form { max-height: 500px; - overflow-y: auto; } .wizard-step-emoji { @@ -115,6 +114,7 @@ body.wizard { .wizard-step-colors { margin-bottom: 20px; + overflow-y: auto; .grid { box-sizing: border-box; display: flex; From fc3b904e1f64b7d53d7c8f11edd8ef434612f46e Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sun, 29 Jul 2018 14:26:24 +0530 Subject: [PATCH 031/168] remove "track external right clicks" feature --- .../discourse/lib/click-track.js.es6 | 9 ---- config/locales/server.en.yml | 1 - config/site_settings.yml | 3 -- ...2926_remove_track_external_right_clicks.rb | 9 ++++ test/javascripts/helpers/site-settings.js | 1 - .../lib/click-track-edit-history-test.js.es6 | 35 -------------- .../lib/click-track-profile-page-test.js.es6 | 46 ------------------- test/javascripts/lib/click-track-test.js.es6 | 35 -------------- 8 files changed, 9 insertions(+), 130 deletions(-) create mode 100644 db/migrate/20180729092926_remove_track_external_right_clicks.rb diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 5a5da15eb4..96ea6f2f9c 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -83,15 +83,6 @@ export default { } } - // If they right clicked, change the destination href - if (e.which === 3) { - $link.attr( - "href", - Discourse.SiteSettings.track_external_right_clicks ? destUrl : href - ); - return true; - } - // if they want to open in a new tab, do an AJAX request if (tracking && wantsNewWindow(e)) { ajax("/clicks/track", { diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2780d6cf9c..2677e0f166 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1227,7 +1227,6 @@ en: post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." share_links: "Determine which items appear on the share dialog, and in what order." - track_external_right_clicks: "Track external links that are right clicked (eg: open in new tab) disabled by default because it rewrites URLs" site_contact_username: "A valid staff username to send all automated messages from. If left blank the default System account will be used." send_welcome_message: "Send all new users a welcome message with a quick start guide." send_tl1_welcome_message: "Send new trust level 1 users a welcome message." diff --git a/config/site_settings.yml b/config/site_settings.yml index 4571cf421c..a6a2a26346 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -98,9 +98,6 @@ basic: default: 365 min: 7 max: 10000 - track_external_right_clicks: - client: true - default: false ga_universal_tracking_code: client: true default: '' diff --git a/db/migrate/20180729092926_remove_track_external_right_clicks.rb b/db/migrate/20180729092926_remove_track_external_right_clicks.rb new file mode 100644 index 0000000000..424804b477 --- /dev/null +++ b/db/migrate/20180729092926_remove_track_external_right_clicks.rb @@ -0,0 +1,9 @@ +class RemoveTrackExternalRightClicks < ActiveRecord::Migration[5.2] + def up + execute "DELETE FROM site_settings WHERE name = 'track_external_right_clicks'" + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/test/javascripts/helpers/site-settings.js b/test/javascripts/helpers/site-settings.js index f84fd111e0..4c8775b5f0 100644 --- a/test/javascripts/helpers/site-settings.js +++ b/test/javascripts/helpers/site-settings.js @@ -7,7 +7,6 @@ Discourse.SiteSettingsOriginal = { "favicon_url":"//meta.discourse.org/uploads/default/2499/79d53726406d87af.ico", "allow_user_locale":false, "suggested_topics":7, - "track_external_right_clicks":false, "ga_universal_tracking_code":"", "ga_universal_domain_name":"auto", "top_menu":"latest|new|unread|categories|top", diff --git a/test/javascripts/lib/click-track-edit-history-test.js.es6 b/test/javascripts/lib/click-track-edit-history-test.js.es6 index 4f9c7d9602..9ad93bb66a 100644 --- a/test/javascripts/lib/click-track-edit-history-test.js.es6 +++ b/test/javascripts/lib/click-track-edit-history-test.js.es6 @@ -103,41 +103,6 @@ asyncTestDiscourse("restores the href after a while", function(assert) { }, 75); }); -var trackRightClick = function(target) { - var clickEvent = generateClickEventOn(target); - clickEvent.which = 3; - return track(clickEvent); -}; - -QUnit.test("right clicks change the href", assert => { - assert.ok(trackRightClick("a")); - assert.equal( - fixture("a") - .first() - .prop("href"), - "http://www.google.com/" - ); -}); - -QUnit.test("right clicks are tracked", assert => { - Discourse.SiteSettings.track_external_right_clicks = true; - trackRightClick("a"); - assert.equal( - fixture("a") - .first() - .attr("href"), - "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" - ); -}); - -QUnit.test("preventDefault is not called for right clicks", assert => { - var clickEvent = generateClickEventOn("a"); - clickEvent.which = 3; - sandbox.stub(clickEvent, "preventDefault"); - assert.ok(track(clickEvent)); - assert.ok(!clickEvent.preventDefault.calledOnce); -}); - var testOpenInANewTab = function(description, clickEventModifier) { test(description, function(assert) { var clickEvent = generateClickEventOn("a"); diff --git a/test/javascripts/lib/click-track-profile-page-test.js.es6 b/test/javascripts/lib/click-track-profile-page-test.js.es6 index fdf3162f13..0b5eafe5ef 100644 --- a/test/javascripts/lib/click-track-profile-page-test.js.es6 +++ b/test/javascripts/lib/click-track-profile-page-test.js.es6 @@ -97,52 +97,6 @@ asyncTestDiscourse("restores the href after a while", function(assert) { }, 75); }); -var trackRightClick = function(target) { - var clickEvent = generateClickEventOn(target); - clickEvent.which = 3; - return track(clickEvent); -}; - -QUnit.test("right clicks change the href", assert => { - assert.ok(trackRightClick("a")); - assert.equal( - fixture("a") - .first() - .prop("href"), - "http://www.google.com/" - ); -}); - -QUnit.test("right clicks are tracked", assert => { - Discourse.SiteSettings.track_external_right_clicks = true; - trackRightClick("a"); - assert.equal( - fixture(".first a") - .first() - .attr("href"), - "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" - ); -}); - -QUnit.test("right clicks are tracked for second excerpt", assert => { - Discourse.SiteSettings.track_external_right_clicks = true; - trackRightClick(".second a"); - assert.equal( - fixture(".second a") - .first() - .attr("href"), - "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331" - ); -}); - -QUnit.test("preventDefault is not called for right clicks", assert => { - var clickEvent = generateClickEventOn("a"); - clickEvent.which = 3; - sandbox.stub(clickEvent, "preventDefault"); - assert.ok(track(clickEvent)); - assert.ok(!clickEvent.preventDefault.calledOnce); -}); - var testOpenInANewTab = function(description, clickEventModifier) { test(description, function(assert) { var clickEvent = generateClickEventOn("a"); diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index 3933768c15..6b2149b983 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -132,41 +132,6 @@ QUnit.test("updates badge counts correctly", function(assert) { badgeClickCount(assert, "with-badge", 2); }); -var trackRightClick = function() { - var clickEvent = generateClickEventOn("a"); - clickEvent.which = 3; - return track(clickEvent); -}; - -QUnit.test("right clicks change the href", function(assert) { - assert.ok(trackRightClick()); - assert.equal( - fixture("a") - .first() - .prop("href"), - "http://www.google.com/" - ); -}); - -QUnit.test("right clicks are tracked", function(assert) { - Discourse.SiteSettings.track_external_right_clicks = true; - trackRightClick(); - assert.equal( - fixture("a") - .first() - .attr("href"), - "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" - ); -}); - -QUnit.test("preventDefault is not called for right clicks", function(assert) { - var clickEvent = generateClickEventOn("a"); - clickEvent.which = 3; - sandbox.stub(clickEvent, "preventDefault"); - assert.ok(track(clickEvent)); - assert.ok(!clickEvent.preventDefault.calledOnce); -}); - var testOpenInANewTab = function(description, clickEventModifier) { test(description, function(assert) { var clickEvent = generateClickEventOn("a"); From b94633e8447e2d1b82d2a3ec9a29bf23f46904f4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 30 Jul 2018 10:48:44 +0800 Subject: [PATCH 032/168] FIX: `FileHelper` should prioritize response content-type. Request to a URL with `.png` extension may return a jpg instead causing us to attach the wrong extension to an upload. --- app/models/user_avatar.rb | 10 +++++++++- lib/file_helper.rb | 6 ++---- spec/components/file_helper_spec.rb | 19 ++++++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index 4774df14f3..731c15981a 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -32,7 +32,15 @@ class UserAvatar < ActiveRecord::Base ) if tempfile - upload = UploadCreator.new(tempfile, 'gravatar.png', origin: gravatar_url, type: "avatar").create_for(user_id) + ext = File.extname(tempfile) + ext = '.png' if ext.blank? + + upload = UploadCreator.new( + tempfile, + "gravatar#{ext}", + origin: gravatar_url, + type: "avatar" + ).create_for(user_id) if gravatar_upload_id != upload.id gravatar_upload&.destroy! diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 4fc4df68e4..9f859ff335 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -54,15 +54,13 @@ class FileHelper end end - # first run - tmp_file_ext = File.extname(uri.path) - - if tmp_file_ext.blank? && response.content_type.present? + if response.content_type.present? ext = MiniMime.lookup_by_content_type(response.content_type)&.extension ext = "jpg" if ext == "jpe" tmp_file_ext = "." + ext if ext.present? end + tmp_file_ext ||= File.extname(uri.path) tmp = Tempfile.new([tmp_file_name, tmp_file_ext]) tmp.binmode end diff --git a/spec/components/file_helper_spec.rb b/spec/components/file_helper_spec.rb index 2ba6cb405e..b481260ed7 100644 --- a/spec/components/file_helper_spec.rb +++ b/spec/components/file_helper_spec.rb @@ -12,7 +12,6 @@ describe FileHelper do end describe "download" do - it "correctly raises an OpenURI HTTP error if it gets a 404 even with redirect" do url = "http://fourohfour.com/404" stub_request(:get, url).to_return(status: 404, body: "404") @@ -69,6 +68,24 @@ describe FileHelper do ) expect(tmpfile.read[0..5]).to eq("GIF89a") end + + describe 'when url is a jpeg' do + let(:url) { "https://eviltrout.com/trout.jpg" } + + it "should prioritize the content type returned by the response" do + stub_request(:get, url).to_return(body: png, headers: { + "content-type": "image/png" + }) + + tmpfile = FileHelper.download( + url, + max_file_size: 10000, + tmp_file_name: 'trouttmp' + ) + + expect(File.extname(tmpfile)).to eq('.png') + end + end end end From 87537b679c74da58e9439c322f009f7bbd09966a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 27 Jul 2018 12:32:07 +0800 Subject: [PATCH 033/168] Drop `reply_key`, `skipped` and `skipped_reason` from `email_logs`. --- app/controllers/admin/email_controller.rb | 11 ++--- app/jobs/scheduled/clean_up_email_logs.rb | 5 +- app/mailers/user_notifications.rb | 2 +- app/models/email_log.rb | 48 +++++++++---------- db/fixtures/000_delayed_drops.rb | 5 +- ..._skipped_skipped_reason_from_email_logs.rb | 11 +++++ spec/jobs/clean_up_email_logs_spec.rb | 2 +- spec/jobs/user_email_spec.rb | 1 - spec/mailers/user_notifications_spec.rb | 7 ++- spec/models/email_log_spec.rb | 35 ++++++-------- 10 files changed, 65 insertions(+), 62 deletions(-) create mode 100644 db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 5ce8f2c170..c626ffb225 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -18,12 +18,11 @@ class Admin::EmailController < Admin::AdminController end def sent - email_logs = EmailLog.sent - .joins(" - LEFT JOIN post_reply_keys - ON post_reply_keys.post_id = email_logs.post_id - AND post_reply_keys.user_id = email_logs.user_id - ") + email_logs = EmailLog.joins(<<~SQL) + LEFT JOIN post_reply_keys + ON post_reply_keys.post_id = email_logs.post_id + AND post_reply_keys.user_id = email_logs.user_id + SQL email_logs = filter_logs(email_logs, params) diff --git a/app/jobs/scheduled/clean_up_email_logs.rb b/app/jobs/scheduled/clean_up_email_logs.rb index 68650829b2..b4e0fafa26 100644 --- a/app/jobs/scheduled/clean_up_email_logs.rb +++ b/app/jobs/scheduled/clean_up_email_logs.rb @@ -8,10 +8,7 @@ module Jobs threshold = SiteSetting.delete_email_logs_after_days.days.ago - EmailLog.where("reply_key IS NULL") - .where("created_at < ?", threshold) - .delete_all - + EmailLog.where("created_at < ?", threshold).delete_all SkippedEmailLog.where("created_at < ?", threshold).delete_all end diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 72eefc6937..cde42b3ee6 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -541,7 +541,7 @@ class UserNotifications < ActionMailer::Base end else reached_limit = SiteSetting.max_emails_per_day_per_user > 0 - reached_limit &&= (EmailLog.where(user_id: user.id, skipped: false) + reached_limit &&= (EmailLog.where(user_id: user.id) .where('created_at > ?', 1.day.ago) .count) >= (SiteSetting.max_emails_per_day_per_user - 1) diff --git a/app/models/email_log.rb b/app/models/email_log.rb index ba588e1b43..08839f290c 100644 --- a/app/models/email_log.rb +++ b/app/models/email_log.rb @@ -4,6 +4,8 @@ class EmailLog < ActiveRecord::Base self.ignored_columns = %w{ topic_id reply_key + skipped + skipped_reason } CRITICAL_EMAIL_TYPES ||= Set.new %w{ @@ -23,20 +25,18 @@ class EmailLog < ActiveRecord::Base validates :email_type, :to_address, presence: true - scope :sent, -> { where(skipped: false) } - scope :skipped, -> { where(skipped: true) } - scope :bounced, -> { sent.where(bounced: true) } + scope :bounced, -> { where(bounced: true) } after_create do # Update last_emailed_at if the user_id is present and email was sent - User.where(id: user_id).update_all("last_emailed_at = CURRENT_TIMESTAMP") if user_id.present? && !skipped + User.where(id: user_id).update_all("last_emailed_at = CURRENT_TIMESTAMP") if user_id.present? end def self.unique_email_per_post(post, user) return yield unless post && user DistributedMutex.synchronize("email_log_#{post.id}_#{user.id}") do - if where(post_id: post.id, user_id: user.id, skipped: false).exists? + if where(post_id: post.id, user_id: user.id).exists? nil else yield @@ -47,7 +47,7 @@ class EmailLog < ActiveRecord::Base def self.reached_max_emails?(user, email_type = nil) return false if SiteSetting.max_emails_per_day_per_user == 0 || CRITICAL_EMAIL_TYPES.include?(email_type) - count = sent.where('created_at > ?', 1.day.ago) + count = where('created_at > ?', 1.day.ago) .where(user_id: user.id) .count @@ -55,7 +55,7 @@ class EmailLog < ActiveRecord::Base end def self.count_per_day(start_date, end_date) - sent.where("created_at BETWEEN ? AND ?", start_date, end_date) + where("created_at BETWEEN ? AND ?", start_date, end_date) .group("DATE(created_at)") .order("DATE(created_at)") .count @@ -83,26 +83,22 @@ end # # Table name: email_logs # -# id :integer not null, primary key -# to_address :string not null -# email_type :string not null -# user_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# post_id :integer -# skipped :boolean default(FALSE) -# skipped_reason :string -# bounce_key :uuid -# bounced :boolean default(FALSE), not null -# message_id :string +# id :integer not null, primary key +# to_address :string not null +# email_type :string not null +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# post_id :integer +# bounce_key :uuid +# bounced :boolean default(FALSE), not null +# message_id :string # # Indexes # -# idx_email_logs_user_created_filtered (user_id,created_at) WHERE (skipped = false) -# index_email_logs_on_created_at (created_at) -# index_email_logs_on_message_id (message_id) -# index_email_logs_on_post_id (post_id) -# index_email_logs_on_reply_key (reply_key) -# index_email_logs_on_skipped_and_bounced_and_created_at (skipped,bounced,created_at) -# index_email_logs_on_user_id (user_id) +# index_email_logs_on_created_at (created_at) +# index_email_logs_on_message_id (message_id) +# index_email_logs_on_post_id (post_id) +# index_email_logs_on_user_id (user_id) +# index_email_logs_on_user_id_and_created_at (user_id,created_at) # diff --git a/db/fixtures/000_delayed_drops.rb b/db/fixtures/000_delayed_drops.rb index edf88f772d..5b976a7bdb 100644 --- a/db/fixtures/000_delayed_drops.rb +++ b/db/fixtures/000_delayed_drops.rb @@ -241,9 +241,12 @@ Migration::ColumnDropper.drop( Migration::ColumnDropper.drop( table: 'email_logs', - after_migration: 'DropTopicIdOnEmailLogs', + after_migration: 'DropReplyKeySkippedSkippedReasonFromEmailLogs', columns: %w{ topic_id + reply_key + skipped + skipped_reason }, on_drop: ->() { STDERR.puts "Removing superflous email_logs columns!" diff --git a/db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb b/db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb new file mode 100644 index 0000000000..48905b0c25 --- /dev/null +++ b/db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb @@ -0,0 +1,11 @@ +class DropReplyKeySkippedSkippedReasonFromEmailLogs < ActiveRecord::Migration[5.2] + def up + remove_index :email_logs, [:skipped, :bounced, :created_at] + remove_index :email_logs, name: 'idx_email_logs_user_created_filtered' + add_index :email_logs, [:user_id, :created_at] + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/spec/jobs/clean_up_email_logs_spec.rb b/spec/jobs/clean_up_email_logs_spec.rb index 59305c492f..e56e1453b7 100644 --- a/spec/jobs/clean_up_email_logs_spec.rb +++ b/spec/jobs/clean_up_email_logs_spec.rb @@ -11,7 +11,7 @@ describe Jobs::CleanUpEmailLogs do Fabricate(:skipped_email_log) end - it "removes old email logs without a reply_key" do + it "removes old email logs" do Jobs::CleanUpEmailLogs.new.execute({}) expect(EmailLog.count).to eq(2) expect(SkippedEmailLog.count).to eq(1) diff --git a/spec/jobs/user_email_spec.rb b/spec/jobs/user_email_spec.rb index 4f26d500ee..7a82606d20 100644 --- a/spec/jobs/user_email_spec.rb +++ b/spec/jobs/user_email_spec.rb @@ -45,7 +45,6 @@ describe Jobs::UserEmail do email_log = EmailLog.where(user_id: user.id).last expect(email_log.email_type).to eq("signup") - expect(email_log.skipped).to eq(false) end end diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index 67033ff401..b144995f1b 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -506,7 +506,12 @@ describe UserNotifications do it 'adds a warning when mail limit is reached' do SiteSetting.max_emails_per_day_per_user = 2 user = Fabricate(:user) - user.email_logs.create(email_type: 'blah', to_address: user.email, user_id: user.id, skipped: false) + + user.email_logs.create!( + email_type: 'blah', + to_address: user.email, + user_id: user.id + ) post = Fabricate(:post) reply = Fabricate(:post, topic_id: post.topic_id) diff --git a/spec/models/email_log_spec.rb b/spec/models/email_log_spec.rb index 5a749dedbd..f0f60dabb0 100644 --- a/spec/models/email_log_spec.rb +++ b/spec/models/email_log_spec.rb @@ -13,23 +13,25 @@ describe EmailLog do post = Fabricate(:post) user = post.user - # skipped emails do not matter - Fabricate(:email_log, user: user, email_type: 'blah', post_id: post.id, to_address: user.email, user_id: user.id, skipped: true) + ran = EmailLog.unique_email_per_post(post, user) do + true + end + + expect(ran).to be(true) + + Fabricate(:email_log, + user: user, + email_type: 'blah', + post_id: post.id, + to_address: user.email, + user_id: user.id + ) ran = EmailLog.unique_email_per_post(post, user) do true end - expect(ran).to eq(true) - - Fabricate(:email_log, user: user, email_type: 'blah', post_id: post.id, to_address: user.email, user_id: user.id) - - ran = EmailLog.unique_email_per_post(post, user) do - true - end - - expect(ran).to be_falsy - + expect(ran).to be(nil) end end @@ -41,20 +43,12 @@ describe EmailLog do user.reload }.to change(user, :last_emailed_at) end - - it "doesn't update last_emailed_at if skipped is true" do - expect { - Fabricate(:email_log, user: user, email_type: 'blah', to_address: user.email, skipped: true) - user.reload - }.to_not change { user.last_emailed_at } - end end end describe '#reached_max_emails?' do before do SiteSetting.max_emails_per_day_per_user = 2 - Fabricate(:email_log, user: user, email_type: 'blah', to_address: user.email, user_id: user.id, skipped: true) Fabricate(:email_log, user: user, email_type: 'blah', to_address: user.email, user_id: user.id) Fabricate(:email_log, user: user, email_type: 'blah', to_address: user.email, user_id: user.id, created_at: 3.days.ago) end @@ -76,7 +70,6 @@ describe EmailLog do describe '#count_per_day' do it "counts sent emails" do Fabricate(:email_log, user: user, email_type: 'blah', to_address: user.email) - Fabricate(:email_log, user: user, email_type: 'blah', to_address: user.email, skipped: true) expect(described_class.count_per_day(1.day.ago, Time.now).first[1]).to eq 1 end end From 96a0448c522f35743b168200442b06ab7ee5deb4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 30 Jul 2018 14:43:44 +0800 Subject: [PATCH 034/168] FIX: Add onceoff job to fix incorrect extension for gravatar uploads. --- app/jobs/onceoff/fix_invalid_gravatar_uploads.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 app/jobs/onceoff/fix_invalid_gravatar_uploads.rb diff --git a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb new file mode 100644 index 0000000000..34c62f8074 --- /dev/null +++ b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb @@ -0,0 +1,14 @@ +module Jobs + class FixInvalidGravatarUploads < Jobs::Onceoff + def execute_onceoff + Upload.where(original_filename: "gravatar.png").find_each do |upload| + extension = FastImage.type(Discourse.store.path_for(upload)) + current_extension = upload.extension + + if extension.to_s.downcase != current_extension.to_s.downcase + upload.user.user_avatar.update_gravatar! + end + end + end + end +end From acde8d4323f95fd7b4122917248c8f542efb4201 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 30 Jul 2018 15:07:03 +0800 Subject: [PATCH 035/168] Fix the build. --- app/jobs/onceoff/fix_invalid_gravatar_uploads.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb index 34c62f8074..1374401101 100644 --- a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb +++ b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb @@ -1,6 +1,6 @@ module Jobs class FixInvalidGravatarUploads < Jobs::Onceoff - def execute_onceoff + def execute_onceoff(args) Upload.where(original_filename: "gravatar.png").find_each do |upload| extension = FastImage.type(Discourse.store.path_for(upload)) current_extension = upload.extension From 78d91b1daf39b34c04c9865578f9f1101064995d Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Mon, 30 Jul 2018 14:13:00 +0530 Subject: [PATCH 036/168] UX: Changes in top categories of hamburger menu (#6200) --- .../widgets/hamburger-categories.js.es6 | 24 ++++----------- .../discourse/widgets/hamburger-menu.js.es6 | 30 +++++++++---------- .../stylesheets/common/base/menu-panel.scss | 8 ----- app/serializers/current_user_serializer.rb | 4 +-- config/locales/client.en.yml | 2 +- config/locales/server.en.yml | 2 ++ config/site_settings.yml | 4 +++ .../widgets/hamburger-menu-test.js.es6 | 14 ++++++--- 8 files changed, 39 insertions(+), 49 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 index af0c7be0ba..d8c3daae92 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 @@ -51,14 +51,15 @@ export default createWidget("hamburger-categories", { html(attrs) { const href = Discourse.getURL("/categories"); + let title = I18n.t("filters.categories.title"); + if (attrs.moreCount > 0) { + title += I18n.t("categories.more", { count: attrs.moreCount }); + } + let result = [ h( "li.heading", - h( - "a.d-link.categories-link", - { attributes: { href } }, - I18n.t("filters.categories.title") - ) + h("a.d-link.categories-link", { attributes: { href } }, title) ) ]; @@ -70,19 +71,6 @@ export default createWidget("hamburger-categories", { categories.map(c => this.attach("hamburger-category", c)) ); - if (attrs.showMore) { - result = result.concat( - h( - "li.footer", - h( - "a.d-link.more-link", - { attributes: { href } }, - I18n.t("categories.more") - ) - ) - ); - } - return result; } }); diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index ef84b58788..3c90d646fe 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -176,28 +176,28 @@ export default createWidget("hamburger-menu", { }, listCategories() { - const maxCategoriesToDisplay = 6; + const maxCategoriesToDisplay = this.siteSettings + .hamburger_menu_categories_count; const categoriesList = this.site.get("categoriesByCount"); - let categories = []; - let showMore = categoriesList.length > maxCategoriesToDisplay; + let categories = categoriesList.slice(); if (this.currentUser) { let categoryIds = this.currentUser.get("top_category_ids") || []; - categoryIds = categoryIds.concat(categoriesList.map(c => c.id)).uniq(); - - showMore = categoryIds.length > maxCategoriesToDisplay; - categoryIds = categoryIds.slice(0, maxCategoriesToDisplay); - - categories = categoryIds.map(id => { - return categoriesList.find(c => c.id === id); + let i = 0; + categoryIds.forEach(id => { + const category = categories.find(c => c.id === id); + if (category) { + categories = categories.filter(c => c.id !== id); + categories.splice(i, 0, category); + i += 1; + } }); - categories = categories.filter(c => c); - } else { - showMore = categoriesList.length > maxCategoriesToDisplay; - categories = categoriesList.slice(0, maxCategoriesToDisplay); } - return this.attach("hamburger-categories", { categories, showMore }); + const moreCount = categories.length - maxCategoriesToDisplay; + categories = categories.slice(0, maxCategoriesToDisplay); + + return this.attach("hamburger-categories", { categories, moreCount }); }, footerLinks(prioritizeFaq, faqUrl) { diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 0d11ebb4f8..18c5083e8d 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -108,14 +108,6 @@ } } - .category-links { - .footer { - clear: both; - display: block; - padding: 0.25em 0.5em; - } - } - // note these topic counts only appear for anons in the category hamburger drop down b.topics-count { color: dark-light-choose($primary-medium, $secondary-medium); diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index a2fffd2187..0b801b0942 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -2,8 +2,6 @@ require_dependency 'new_post_manager' class CurrentUserSerializer < BasicUserSerializer - MAX_TOP_CATEGORIES_COUNT = 6.freeze - attributes :name, :unread_notifications, :unread_private_messages, @@ -169,7 +167,7 @@ class CurrentUserSerializer < BasicUserSerializer WHEN notification_level = 4 THEN 3 END") .pluck(:category_id) - .slice(0, MAX_TOP_CATEGORIES_COUNT) + .slice(0, SiteSetting.hamburger_menu_categories_count) end def dismissed_banner_key diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 1d36c4c642..fc13eed978 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -571,7 +571,7 @@ en: topic_stat_sentence: one: "%{count} new topic in the past %{unit}." other: "%{count} new topics in the past %{unit}." - more: "more" + more: " (%{count} more) ..." ip_lookup: title: IP Address Lookup diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2677e0f166..8c85dc3f75 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1655,6 +1655,8 @@ en: suppress_uncategorized_badge: "Don't show the badge for uncategorized topics in topic lists." + hamburger_menu_categories_count: "How many categories can be displayed in the hamburger menu." + permalink_normalizations: "Apply the following regex before matching permalinks, for example: /(topic.*)\\?.*/\\1 will strip query strings from topic routes. Format is regex+string use \\1 etc. to access captures" global_notice: "Display an URGENT, EMERGENCY global banner notice to all visitors, change to blank to hide it (HTML allowed)." diff --git a/config/site_settings.yml b/config/site_settings.yml index a6a2a26346..1aabd59c5d 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1415,6 +1415,10 @@ uncategorized: client: true default: true + hamburger_menu_categories_count: + client: true + default: 8 + slug_generation_method: default: 'ascii' enum: 'SlugSetting' diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 index 04d30f5369..e5434af35a 100644 --- a/test/javascripts/widgets/hamburger-menu-test.js.es6 +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -2,7 +2,6 @@ import { moduleForWidget, widgetTest } from "helpers/widget-test"; moduleForWidget("hamburger-menu"); -const maxCategoriesToDisplay = 6; const topCategoryIds = [2, 3, 1]; widgetTest("prioritize faq", { @@ -128,10 +127,17 @@ widgetTest("general links", { } }); +let maxCategoriesToDisplay; + widgetTest("top categories - anonymous", { template: '{{mount-widget widget="hamburger-menu"}}', anonymous: true, + beforeEach() { + this.siteSettings.hamburger_menu_categories_count = 8; + maxCategoriesToDisplay = this.siteSettings.hamburger_menu_categories_count; + }, + test(assert) { const count = this.site.get("categoriesByCount").length; const maximum = @@ -144,15 +150,15 @@ widgetTest("top categories", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { + this.siteSettings.hamburger_menu_categories_count = 8; + maxCategoriesToDisplay = this.siteSettings.hamburger_menu_categories_count; this.currentUser.set("top_category_ids", topCategoryIds); }, test(assert) { assert.equal(find(".category-link").length, maxCategoriesToDisplay); - const categoriesList = this.site - .get("categoriesByCount") - .reject(c => c.parent_category_id); + const categoriesList = this.site.get("categoriesByCount"); let ids = topCategoryIds .concat(categoriesList.map(c => c.id)) .uniq() From ba64ebbf103d0d55d6f24dc6e477cd3abd0c552b Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 30 Jul 2018 14:48:37 +0530 Subject: [PATCH 037/168] FIX: preserve whitespace between uploads when the process is complete --- .../javascripts/discourse/components/composer-editor.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 390b5f7f28..a93267c669 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -632,7 +632,7 @@ export default Ember.Component.extend({ cacheShortUploadUrl(upload.short_url, upload.url); this.appEvents.trigger( "composer:replace-text", - uploadPlaceholder, + uploadPlaceholder.trim(), markdown ); this._resetUpload(false); From ef78268c01edf92c7477166d3e7d4b071992b7b2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 30 Jul 2018 17:11:20 +0800 Subject: [PATCH 038/168] Give `ExtraNavItem` more control over when it can be displayed. --- .../javascripts/discourse/lib/plugin-api.js.es6 | 12 ++++++++++++ .../javascripts/discourse/models/nav-item.js.es6 | 9 +++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index cfff459ed9..6c20b968f5 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -668,6 +668,18 @@ class PluginApi { * displayName: "Discourse" * href: "https://www.discourse.org", * }) + * + * An optional `customFilter` callback can be included to not display the + * nav item on certain routes + * + * Example: + * + * addNavigationBarItem({ + * name: "link-to-bugs-category", + * displayName: "bugs" + * href: "/c/bugs", + * customFilter: (category, args) => { category && category.get('name') !== 'bug' } + * }) */ addNavigationBarItem(item) { if (!item["name"]) { diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index 4587183ed6..e6ad1f9102 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -102,7 +102,8 @@ const NavItem = Discourse.Model.extend({ }); const ExtraNavItem = NavItem.extend({ - @computed("href") href: href => href + @computed("href") href: href => href, + customFilter: null }); NavItem.reopenClass({ @@ -169,7 +170,11 @@ NavItem.reopenClass({ i => i !== null && !(category && i.get("name").indexOf("categor") === 0) ); - return items.concat(NavItem.extraNavItems); + const extraItems = NavItem.extraNavItems.filter(item => { + return item.customFilter && item.customFilter.call(this, category, args); + }); + + return items.concat(extraItems); } }); From 581cf62bcfc1d25965c4f51679b95a7c9ee07875 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 30 Jul 2018 18:02:34 +0800 Subject: [PATCH 039/168] UX: Allow emojis to be displayed in nav-item. --- app/assets/javascripts/discourse/models/nav-item.js.es6 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index e6ad1f9102..ad4c0b1695 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -1,4 +1,5 @@ import { toTitleCase } from "discourse/lib/formatter"; +import { emojiUnescape } from "discourse/lib/text"; import computed from "ember-addons/ember-computed-decorators"; const NavItem = Discourse.Model.extend({ @@ -30,7 +31,9 @@ const NavItem = Discourse.Model.extend({ extra.categoryName = toTitleCase(categoryName); } - return I18n.t(`filters.${name.replace("/", ".") + titleKey}`, extra); + return emojiUnescape( + I18n.t(`filters.${name.replace("/", ".") + titleKey}`, extra) + ); }, @computed("name") From e9856522dcea50cb61b465e0beb9971890e98e82 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 30 Jul 2018 11:06:14 +0100 Subject: [PATCH 040/168] UX: Add 'when' to autobump messages --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fc13eed978..3f5144742c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -152,7 +152,7 @@ en: user_left: "%{who} removed themselves from this message %{when}" removed_user: "removed %{who} %{when}" removed_group: "removed %{who} %{when}" - autobumped: "automatically bumped topic" + autobumped: "automatically bumped topic %{when}" autoclosed: enabled: 'closed %{when}' disabled: 'opened %{when}' From 249b16e8e3417cb45cc0209220f509573e9d22d6 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Mon, 30 Jul 2018 15:37:41 +0530 Subject: [PATCH 041/168] FIX: Hide muted categories from hamburger menu top categories block --- .../discourse/widgets/hamburger-menu.js.es6 | 2 ++ .../widgets/hamburger-menu-test.js.es6 | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 3c90d646fe..3838d675e6 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -184,6 +184,8 @@ export default createWidget("hamburger-menu", { if (this.currentUser) { let categoryIds = this.currentUser.get("top_category_ids") || []; let i = 0; + const mutedCategoryIds = this.currentUser.get("muted_category_ids") || []; + categories = categories.filter(c => !mutedCategoryIds.includes(c.id)); categoryIds.forEach(id => { const category = categories.find(c => c.id === id); if (category) { diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 index e5434af35a..ddb39f219d 100644 --- a/test/javascripts/widgets/hamburger-menu-test.js.es6 +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -3,6 +3,8 @@ import { moduleForWidget, widgetTest } from "helpers/widget-test"; moduleForWidget("hamburger-menu"); const topCategoryIds = [2, 3, 1]; +let mutedCategoryIds = []; +let categoriesByCount = []; widgetTest("prioritize faq", { template: '{{mount-widget widget="hamburger-menu"}}', @@ -136,10 +138,11 @@ widgetTest("top categories - anonymous", { beforeEach() { this.siteSettings.hamburger_menu_categories_count = 8; maxCategoriesToDisplay = this.siteSettings.hamburger_menu_categories_count; + categoriesByCount = this.site.get("categoriesByCount"); }, test(assert) { - const count = this.site.get("categoriesByCount").length; + const count = categoriesByCount.length; const maximum = count <= maxCategoriesToDisplay ? count : maxCategoriesToDisplay; assert.equal(find(".category-link").length, maximum); @@ -152,21 +155,32 @@ widgetTest("top categories", { beforeEach() { this.siteSettings.hamburger_menu_categories_count = 8; maxCategoriesToDisplay = this.siteSettings.hamburger_menu_categories_count; + categoriesByCount = this.site.get("categoriesByCount"); + categoriesByCount.every(c => { + if (!topCategoryIds.includes(c.id)) { + mutedCategoryIds.push(c.id); + return false; + } + return true; + }); this.currentUser.set("top_category_ids", topCategoryIds); + this.currentUser.set("muted_category_ids", mutedCategoryIds); }, test(assert) { assert.equal(find(".category-link").length, maxCategoriesToDisplay); - const categoriesList = this.site.get("categoriesByCount"); + categoriesByCount = categoriesByCount.filter( + c => !mutedCategoryIds.includes(c.id) + ); let ids = topCategoryIds - .concat(categoriesList.map(c => c.id)) + .concat(categoriesByCount.map(c => c.id)) .uniq() .slice(0, maxCategoriesToDisplay); assert.equal( find(".category-link .category-name").text(), - ids.map(i => categoriesList.find(c => c.id === i).name).join("") + ids.map(i => categoriesByCount.find(c => c.id === i).name).join("") ); } }); From 04baddf73115ee7f128258c210bbc853f21aaf24 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Sun, 29 Jul 2018 22:51:32 +0200 Subject: [PATCH 042/168] DEV: migrate tests to async/await --- .eslintrc | 1 + .../wizard/test/acceptance/wizard-test.js.es6 | 94 +- .../test/components/invite-list-test.js.es6 | 97 +- .../acceptance/details-button-test.js.es6 | 212 ++-- .../javascripts/acceptance/polls-test.js.es6 | 106 +- .../acceptance/admin-flags-test.js.es6 | 24 +- .../acceptance/admin-suspend-user-test.js.es6 | 4 +- .../acceptance/category-chooser-test.js.es6 | 2 +- .../category-edit-security-test.js.es6 | 16 +- .../acceptance/category-edit-test.js.es6 | 4 +- .../acceptance/composer-actions-test.js.es6 | 44 +- .../acceptance/composer-test.js.es6 | 16 +- .../acceptance/preferences-test.js.es6 | 9 +- .../acceptance/search-full-test.js.es6 | 16 +- .../javascripts/acceptance/search-test.js.es6 | 4 +- .../acceptance/topic-edit-timer-test.js.es6 | 53 +- .../topic-notifications-button-test.js.es6 | 4 +- test/javascripts/acceptance/topic-test.js.es6 | 4 +- .../categories-admin-dropdown-test.js.es6 | 2 +- .../components/category-chooser-test.js.es6 | 4 +- .../components/category-selector-test.js.es6 | 110 +-- .../components/combo-box-test.js.es6 | 22 +- .../components/d-editor-test.js.es6 | 917 +++++++----------- .../components/list-setting-test.js.es6 | 87 +- .../components/multi-select-test.js.es6 | 332 +++---- .../components/single-select-test.js.es6 | 723 ++++++-------- .../topic-footer-mobile-dropdown-test.js.es6 | 4 +- .../topic-notifications-options-test.js.es6 | 4 +- test/javascripts/helpers/d-editor-helper.js | 15 + test/javascripts/helpers/select-kit-helper.js | 118 +-- test/javascripts/test_helper.js | 1 + 31 files changed, 1320 insertions(+), 1729 deletions(-) create mode 100644 test/javascripts/helpers/d-editor-helper.js diff --git a/.eslintrc b/.eslintrc index 4d652d710a..844933e68b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -48,6 +48,7 @@ "selectKitSelectRowByValue":true, "selectKitSelectRowByName":true, "selectKitSelectRowByIndex":true, + "keyboardHelper":true, "selectKitSelectNoneRow":true, "selectKitFillInFilter":true, "asyncTestDiscourse":true, diff --git a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 index 089657bf81..406ba3551b 100644 --- a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 +++ b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 @@ -11,69 +11,57 @@ QUnit.module("Acceptance: wizard", { } }); -test("Wizard starts", assert => { - visit("/"); - andThen(() => { - assert.ok(exists(".wizard-column-contents")); - assert.equal(currentPath(), "step"); - }); +test("Wizard starts", async assert => { + await visit("/"); + assert.ok(exists(".wizard-column-contents")); + assert.equal(currentPath(), "step"); }); -test("Going back and forth in steps", assert => { - visit("/steps/hello-world"); - andThen(() => { - assert.ok(exists(".wizard-step")); - assert.ok( - exists(".wizard-step-hello-world"), - "it adds a class for the step id" - ); +test("Going back and forth in steps", async assert => { + await visit("/steps/hello-world"); + assert.ok(exists(".wizard-step")); + assert.ok( + exists(".wizard-step-hello-world"), + "it adds a class for the step id" + ); - assert.ok(exists(".wizard-progress")); - assert.ok(exists(".wizard-step-title")); - assert.ok(exists(".wizard-step-description")); - assert.ok( - !exists(".invalid .field-full-name"), - "don't show it as invalid until the user does something" - ); - assert.ok(exists(".wizard-field .field-description")); - assert.ok(!exists(".wizard-btn.back")); - assert.ok(!exists(".wizard-field .field-error-description")); - }); + assert.ok(exists(".wizard-progress")); + assert.ok(exists(".wizard-step-title")); + assert.ok(exists(".wizard-step-description")); + assert.ok( + !exists(".invalid .field-full-name"), + "don't show it as invalid until the user does something" + ); + assert.ok(exists(".wizard-field .field-description")); + assert.ok(!exists(".wizard-btn.back")); + assert.ok(!exists(".wizard-field .field-error-description")); // invalid data - click(".wizard-btn.next"); - andThen(() => { - assert.ok(exists(".invalid .field-full-name")); - }); + await click(".wizard-btn.next"); + assert.ok(exists(".invalid .field-full-name")); // server validation fail - fillIn("input.field-full-name", "Server Fail"); - click(".wizard-btn.next"); - andThen(() => { - assert.ok(exists(".invalid .field-full-name")); - assert.ok(exists(".wizard-field .field-error-description")); - }); + await fillIn("input.field-full-name", "Server Fail"); + await click(".wizard-btn.next"); + assert.ok(exists(".invalid .field-full-name")); + assert.ok(exists(".wizard-field .field-error-description")); // server validation ok - fillIn("input.field-full-name", "Evil Trout"); - click(".wizard-btn.next"); - andThen(() => { - assert.ok(!exists(".wizard-field .field-error-description")); - assert.ok(!exists(".wizard-step-title")); - assert.ok(!exists(".wizard-step-description")); + await fillIn("input.field-full-name", "Evil Trout"); + await click(".wizard-btn.next"); + assert.ok(!exists(".wizard-field .field-error-description")); + assert.ok(!exists(".wizard-step-title")); + assert.ok(!exists(".wizard-step-description")); - assert.ok(exists(".select-kit.field-snack"), "went to the next step"); - assert.ok(exists(".preview-area"), "renders the component field"); + assert.ok(exists(".select-kit.field-snack"), "went to the next step"); + assert.ok(exists(".preview-area"), "renders the component field"); - assert.ok(!exists(".wizard-btn.next")); - assert.ok(exists(".wizard-btn.done"), "last step shows a done button"); - assert.ok(exists(".action-link.back"), "shows the back button"); - }); + assert.ok(!exists(".wizard-btn.next")); + assert.ok(exists(".wizard-btn.done"), "last step shows a done button"); + assert.ok(exists(".action-link.back"), "shows the back button"); - click(".action-link.back"); - andThen(() => { - assert.ok(exists(".wizard-step-title")); - assert.ok(exists(".wizard-btn.next")); - assert.ok(!exists(".wizard-prev")); - }); + await click(".action-link.back"); + assert.ok(exists(".wizard-step-title")); + assert.ok(exists(".wizard-btn.next")); + assert.ok(!exists(".wizard-prev")); }); diff --git a/app/assets/javascripts/wizard/test/components/invite-list-test.js.es6 b/app/assets/javascripts/wizard/test/components/invite-list-test.js.es6 index 53e1aa6b3f..95436fb69e 100644 --- a/app/assets/javascripts/wizard/test/components/invite-list-test.js.es6 +++ b/app/assets/javascripts/wizard/test/components/invite-list-test.js.es6 @@ -8,7 +8,7 @@ componentTest("can add users", { this.set("field", {}); }, - test(assert) { + async test(assert) { assert.ok( this.$(".users-list .invite-list-user").length === 0, "no users at first" @@ -26,67 +26,54 @@ componentTest("can add users", { "it has a warning since no users were added" ); - click(".add-user"); - andThen(() => { - assert.ok( - this.$(".users-list .invite-list-user").length === 0, - "doesn't add a blank user" - ); - assert.ok(this.$(".new-user .invalid").length === 1); - }); + await click(".add-user"); + assert.ok( + this.$(".users-list .invite-list-user").length === 0, + "doesn't add a blank user" + ); + assert.ok(this.$(".new-user .invalid").length === 1); - fillIn(".invite-email", "eviltrout@example.com"); - click(".add-user"); + await fillIn(".invite-email", "eviltrout@example.com"); + await click(".add-user"); - andThen(() => { - assert.ok( - this.$(".users-list .invite-list-user").length === 1, - "adds the user" - ); - assert.ok(this.$(".new-user .invalid").length === 0); + assert.ok( + this.$(".users-list .invite-list-user").length === 1, + "adds the user" + ); + assert.ok(this.$(".new-user .invalid").length === 0); - const val = JSON.parse(this.get("field.value")); - assert.equal(val.length, 1); - assert.equal( - val[0].email, - "eviltrout@example.com", - "adds the email to the JSON" - ); - assert.ok(val[0].role.length, "adds the role to the JSON"); - assert.ok( - !this.get("field.warning"), - "no warning once the user is added" - ); - }); + const val = JSON.parse(this.get("field.value")); + assert.equal(val.length, 1); + assert.equal( + val[0].email, + "eviltrout@example.com", + "adds the email to the JSON" + ); + assert.ok(val[0].role.length, "adds the role to the JSON"); + assert.ok(!this.get("field.warning"), "no warning once the user is added"); - fillIn(".invite-email", "eviltrout@example.com"); - click(".add-user"); + await fillIn(".invite-email", "eviltrout@example.com"); + await click(".add-user"); - andThen(() => { - assert.ok( - this.$(".users-list .invite-list-user").length === 1, - "can't add the same user twice" - ); - assert.ok(this.$(".new-user .invalid").length === 1); - }); + assert.ok( + this.$(".users-list .invite-list-user").length === 1, + "can't add the same user twice" + ); + assert.ok(this.$(".new-user .invalid").length === 1); - fillIn(".invite-email", "not-an-email"); - click(".add-user"); + await fillIn(".invite-email", "not-an-email"); + await click(".add-user"); - andThen(() => { - assert.ok( - this.$(".users-list .invite-list-user").length === 1, - "won't add an invalid email" - ); - assert.ok(this.$(".new-user .invalid").length === 1); - }); + assert.ok( + this.$(".users-list .invite-list-user").length === 1, + "won't add an invalid email" + ); + assert.ok(this.$(".new-user .invalid").length === 1); - click(".invite-list .invite-list-user:eq(0) .remove-user"); - andThen(() => { - assert.ok( - this.$(".users-list .invite-list-user").length === 0, - "removed the user" - ); - }); + await click(".invite-list .invite-list-user:eq(0) .remove-user"); + assert.ok( + this.$(".users-list .invite-list-user").length === 0, + "removed the user" + ); } }); diff --git a/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 b/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 index bbc19b5959..08922bd862 100644 --- a/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 +++ b/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 @@ -8,148 +8,126 @@ acceptance("Details Button", { } }); -function findTextarea() { - return find(".d-editor-input")[0]; -} - -test("details button", assert => { +test("details button", async assert => { const popupMenu = selectKit(".toolbar-popup-menu-options"); - visit("/"); - click("#create-topic"); + await visit("/"); + await click("#create-topic"); - popupMenu.expand().selectRowByValue("insertDetails"); + await popupMenu.expand(); + await popupMenu.selectRowByValue("insertDetails"); - andThen(() => { - assert.equal( - find(".d-editor-input").val(), - `\n[details="${I18n.t("composer.details_title")}"]\n${I18n.t( - "composer.details_text" - )}\n[/details]\n`, - "it should contain the right output" - ); - }); + assert.equal( + find(".d-editor-input").val(), + `\n[details="${I18n.t("composer.details_title")}"]\n${I18n.t( + "composer.details_text" + )}\n[/details]\n`, + "it should contain the right output" + ); - fillIn(".d-editor-input", "This is my title"); + await fillIn(".d-editor-input", "This is my title"); - andThen(() => { - const textarea = findTextarea(); - textarea.selectionStart = 0; - textarea.selectionEnd = textarea.value.length; - }); + const textarea = find(".d-editor-input")[0]; + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; - popupMenu.expand().selectRowByValue("insertDetails"); + await popupMenu.expand(); + await popupMenu.selectRowByValue("insertDetails"); - andThen(() => { - assert.equal( - find(".d-editor-input").val(), - `\n[details="${I18n.t( - "composer.details_title" - )}"]\nThis is my title\n[/details]\n`, - "it should contain the right selected output" - ); + assert.equal( + find(".d-editor-input").val(), + `\n[details="${I18n.t( + "composer.details_title" + )}"]\nThis is my title\n[/details]\n`, + "it should contain the right selected output" + ); - const textarea = findTextarea(); - assert.equal( - textarea.selectionStart, - 21, - "it should start highlighting at the right position" - ); - assert.equal( - textarea.selectionEnd, - 37, - "it should end highlighting at the right position" - ); - }); + assert.equal( + textarea.selectionStart, + 21, + "it should start highlighting at the right position" + ); + assert.equal( + textarea.selectionEnd, + 37, + "it should end highlighting at the right position" + ); - fillIn(".d-editor-input", "Before some text in between After"); + await fillIn(".d-editor-input", "Before some text in between After"); - andThen(() => { - const textarea = findTextarea(); - textarea.selectionStart = 7; - textarea.selectionEnd = 28; - }); + textarea.selectionStart = 7; + textarea.selectionEnd = 28; - popupMenu.expand().selectRowByValue("insertDetails"); + await popupMenu.expand(); + await popupMenu.selectRowByValue("insertDetails"); - andThen(() => { - assert.equal( - find(".d-editor-input").val(), - `Before \n[details="${I18n.t( - "composer.details_title" - )}"]\nsome text in between\n[/details]\n After`, - "it should contain the right output" - ); + assert.equal( + find(".d-editor-input").val(), + `Before \n[details="${I18n.t( + "composer.details_title" + )}"]\nsome text in between\n[/details]\n After`, + "it should contain the right output" + ); - const textarea = findTextarea(); - assert.equal( - textarea.selectionStart, - 28, - "it should start highlighting at the right position" - ); - assert.equal( - textarea.selectionEnd, - 48, - "it should end highlighting at the right position" - ); - }); + assert.equal( + textarea.selectionStart, + 28, + "it should start highlighting at the right position" + ); + assert.equal( + textarea.selectionEnd, + 48, + "it should end highlighting at the right position" + ); - fillIn(".d-editor-input", "Before \nsome text in between\n After"); + await fillIn(".d-editor-input", "Before \nsome text in between\n After"); - andThen(() => { - const textarea = findTextarea(); - textarea.selectionStart = 8; - textarea.selectionEnd = 29; - }); + textarea.selectionStart = 8; + textarea.selectionEnd = 29; - popupMenu.expand().selectRowByValue("insertDetails"); + await popupMenu.expand(); + await popupMenu.selectRowByValue("insertDetails"); - andThen(() => { - assert.equal( - find(".d-editor-input").val(), - `Before \n\n[details="${I18n.t( - "composer.details_title" - )}"]\nsome text in between\n[/details]\n\n After`, - "it should contain the right output" - ); + assert.equal( + find(".d-editor-input").val(), + `Before \n\n[details="${I18n.t( + "composer.details_title" + )}"]\nsome text in between\n[/details]\n\n After`, + "it should contain the right output" + ); - const textarea = findTextarea(); - assert.equal( - textarea.selectionStart, - 29, - "it should start highlighting at the right position" - ); - assert.equal( - textarea.selectionEnd, - 49, - "it should end highlighting at the right position" - ); - }); + assert.equal( + textarea.selectionStart, + 29, + "it should start highlighting at the right position" + ); + assert.equal( + textarea.selectionEnd, + 49, + "it should end highlighting at the right position" + ); }); -test("details button surrounds all selected text in a single details block", assert => { +test("details button surrounds all selected text in a single details block", async assert => { const multilineInput = "first line\n\nsecond line\n\nthird line"; const popupMenu = selectKit(".toolbar-popup-menu-options"); - visit("/"); - click("#create-topic"); - fillIn(".d-editor-input", multilineInput); + await visit("/"); + await click("#create-topic"); + await fillIn(".d-editor-input", multilineInput); - andThen(() => { - const textarea = findTextarea(); - textarea.selectionStart = 0; - textarea.selectionEnd = textarea.value.length; - }); + const textarea = find(".d-editor-input")[0]; + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; - popupMenu.expand().selectRowByValue("insertDetails"); + await popupMenu.expand(); + await popupMenu.selectRowByValue("insertDetails"); - andThen(() => { - assert.equal( - find(".d-editor-input").val(), - `\n[details="${I18n.t( - "composer.details_title" - )}"]\n${multilineInput}\n[/details]\n`, - "it should contain the right output" - ); - }); + assert.equal( + find(".d-editor-input").val(), + `\n[details="${I18n.t( + "composer.details_title" + )}"]\n${multilineInput}\n[/details]\n`, + "it should contain the right output" + ); }); diff --git a/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 index f32937ea97..ebfe91455e 100644 --- a/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 @@ -9,7 +9,7 @@ acceptance("Rendering polls", { } }); -test("Single Poll", assert => { +test("Single Poll", async assert => { // prettier-ignore server.get("/t/13.json", () => { // eslint-disable-line no-undef return [ @@ -313,26 +313,24 @@ test("Single Poll", assert => { ]; }); - visit("/t/this-is-a-test-topic-for-polls/13"); + await visit("/t/this-is-a-test-topic-for-polls/13"); - andThen(() => { - const polls = find(".poll"); + const polls = find(".poll"); - assert.equal(polls.length, 2, "it should render the polls correctly"); - assert.equal( - find(".info-number", polls[0]).text(), - "2", - "it should display the right number of votes" - ); - assert.equal( - find(".info-number", polls[1]).text(), - "3", - "it should display the right number of votes" - ); - }); + assert.equal(polls.length, 2, "it should render the polls correctly"); + assert.equal( + find(".info-number", polls[0]).text(), + "2", + "it should display the right number of votes" + ); + assert.equal( + find(".info-number", polls[1]).text(), + "3", + "it should display the right number of votes" + ); }); -test("Public poll", assert => { +test("Public poll", async assert => { // prettier-ignore server.get("/t/12.json", () => { // eslint-disable-line no-undef return [ @@ -1328,35 +1326,29 @@ test("Public poll", assert => { return [200, { "Content-Type": "application/json" }, body]; }); - visit("/t/this-is-a-topic-created-for-testing/12"); + await visit("/t/this-is-a-topic-created-for-testing/12"); - andThen(() => { - const polls = find(".poll"); - assert.equal(polls.length, 1, "it should render the poll correctly"); - }); + const polls = find(".poll"); + assert.equal(polls.length, 1, "it should render the poll correctly"); - click("button.toggle-results"); + await click("button.toggle-results"); - andThen(() => { - assert.equal( - find(".poll-voters:first li").length, - 25, - "it should display the right number of voters" - ); - }); + assert.equal( + find(".poll-voters:first li").length, + 25, + "it should display the right number of voters" + ); - click(".poll-voters-toggle-expand:first a"); + await click(".poll-voters-toggle-expand:first a"); - andThen(() => { - assert.equal( - find(".poll-voters:first li").length, - 50, - "it should display the right number of voters" - ); - }); + assert.equal( + find(".poll-voters:first li").length, + 50, + "it should display the right number of voters" + ); }); -test("Public number poll", assert => { +test("Public number poll", async assert => { // prettier-ignore server.get("/t/13.json", () => { // eslint-disable-line no-undef return [ @@ -2016,30 +2008,24 @@ test("Public number poll", assert => { return [200, { "Content-Type": "application/json" }, body]; }); - visit("/t/this-is-a-topic-for-testing-number-poll/13"); + await visit("/t/this-is-a-topic-for-testing-number-poll/13"); - andThen(() => { - const polls = find(".poll"); - assert.equal(polls.length, 1, "it should render the poll correctly"); - }); + const polls = find(".poll"); + assert.equal(polls.length, 1, "it should render the poll correctly"); - click("button.toggle-results"); + await click("button.toggle-results"); - andThen(() => { - assert.equal( - find(".poll-voters:first li").length, - 25, - "it should display the right number of voters" - ); - }); + assert.equal( + find(".poll-voters:first li").length, + 25, + "it should display the right number of voters" + ); - click(".poll-voters-toggle-expand:first a"); + await click(".poll-voters-toggle-expand:first a"); - andThen(() => { - assert.equal( - find(".poll-voters:first li").length, - 35, - "it should display the right number of voters" - ); - }); + assert.equal( + find(".poll-voters:first li").length, + 35, + "it should display the right number of voters" + ); }); diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index b50280d424..0ce460f4a7 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -24,8 +24,8 @@ QUnit.test("flagged posts - agree", async assert => { await visit("/admin/flags/active"); - await agreeFlag.expandAwait(); - await agreeFlag.selectRowByValueAwait("confirm-agree-keep"); + await agreeFlag.expand(); + await agreeFlag.selectRowByValue("confirm-agree-keep"); assert.equal( find(".admin-flags .flagged-post").length, @@ -39,8 +39,8 @@ QUnit.test("flagged posts - agree + hide", async assert => { await visit("/admin/flags/active"); - await agreeFlag.expandAwait(); - await agreeFlag.selectRowByValueAwait("confirm-agree-hide"); + await agreeFlag.expand(); + await agreeFlag.selectRowByValue("confirm-agree-hide"); assert.equal( find(".admin-flags .flagged-post").length, @@ -54,8 +54,8 @@ QUnit.test("flagged posts - agree + deleteSpammer", async assert => { await visit("/admin/flags/active"); - await agreeFlag.expandAwait(); - await agreeFlag.selectRowByValueAwait("delete-spammer"); + await agreeFlag.expand(); + await agreeFlag.selectRowByValue("delete-spammer"); await click(".confirm-delete"); @@ -85,8 +85,8 @@ QUnit.test("flagged posts - delete + defer", async assert => { await visit("/admin/flags/active"); - await deleteFlag.expandAwait(); - await deleteFlag.selectRowByValueAwait("delete-defer"); + await deleteFlag.expand(); + await deleteFlag.selectRowByValue("delete-defer"); assert.equal(find(".admin-flags .flagged-post").length, 0); }); @@ -96,8 +96,8 @@ QUnit.test("flagged posts - delete + agree", async assert => { await visit("/admin/flags/active"); - await deleteFlag.expandAwait(); - await deleteFlag.selectRowByValueAwait("delete-agree"); + await deleteFlag.expand(); + await deleteFlag.selectRowByValue("delete-agree"); assert.equal(find(".admin-flags .flagged-post").length, 0); }); @@ -107,8 +107,8 @@ QUnit.test("flagged posts - delete + deleteSpammer", async assert => { await visit("/admin/flags/active"); - await deleteFlag.expandAwait(); - await deleteFlag.selectRowByValueAwait("delete-spammer"); + await deleteFlag.expand(); + await deleteFlag.selectRowByValue("delete-spammer"); await click(".confirm-delete"); diff --git a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 index 40da286a81..a982bc74a8 100644 --- a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 +++ b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 @@ -50,8 +50,8 @@ QUnit.test("suspend, then unsuspend a user", async assert => { "disabled by default" ); - await suspendUntilCombobox.expandAwait(); - await suspendUntilCombobox.selectRowByValueAwait("tomorrow"); + await suspendUntilCombobox.expand(); + await suspendUntilCombobox.selectRowByValue("tomorrow"); await fillIn(".suspend-reason", "for breaking the rules"); await fillIn(".suspend-message", "this is an email reason why"); diff --git a/test/javascripts/acceptance/category-chooser-test.js.es6 b/test/javascripts/acceptance/category-chooser-test.js.es6 index d9aabcb9e0..0a1423a25d 100644 --- a/test/javascripts/acceptance/category-chooser-test.js.es6 +++ b/test/javascripts/acceptance/category-chooser-test.js.es6 @@ -13,7 +13,7 @@ QUnit.test("does not display uncategorized if not allowed", async assert => { await visit("/"); await click("#create-topic"); - await categoryChooser.expandAwait(); + await categoryChooser.expand(); assert.ok(categoryChooser.rowByIndex(0).name() !== "uncategorized"); }); diff --git a/test/javascripts/acceptance/category-edit-security-test.js.es6 b/test/javascripts/acceptance/category-edit-security-test.js.es6 index 158de71336..6a4ca5856c 100644 --- a/test/javascripts/acceptance/category-edit-security-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-security-test.js.es6 @@ -33,7 +33,7 @@ QUnit.test("removing a permission", async assert => { await click(".edit-category"); await click("li.edit-category-security a"); await click(".edit-category-tab-security .edit-permission"); - await availableGroups.expandAwait(); + await availableGroups.expand(); assert.notOk( availableGroups.rowByValue("everyone").exists(), @@ -43,7 +43,7 @@ QUnit.test("removing a permission", async assert => { await click( ".edit-category-tab-security .permission-list li:first-of-type .remove-permission" ); - await availableGroups.expandAwait(); + await availableGroups.expand(); assert.ok( availableGroups.rowByValue("everyone").exists(), @@ -60,10 +60,10 @@ QUnit.test("adding a permission", async assert => { await click(".edit-category"); await click("li.edit-category-security a"); await click(".edit-category-tab-security .edit-permission"); - await availableGroups.expandAwait(); - await availableGroups.selectRowByValueAwait("staff"); - await permissionSelector.expandAwait(); - await permissionSelector.selectRowByValueAwait("2"); + await availableGroups.expand(); + await availableGroups.selectRowByValue("staff"); + await permissionSelector.expand(); + await permissionSelector.selectRowByValue("2"); await click(".edit-category-tab-security .add-permission"); const $addedPermissionItem = find( @@ -95,8 +95,8 @@ QUnit.test("adding a previously removed permission", async assert => { "it removes the permission from the list" ); - await availableGroups.expandAwait(); - await availableGroups.selectRowByValueAwait("everyone"); + await availableGroups.expand(); + await availableGroups.selectRowByValue("everyone"); await click(".edit-category-tab-security .add-permission"); assert.equal( diff --git a/test/javascripts/acceptance/category-edit-test.js.es6 b/test/javascripts/acceptance/category-edit-test.js.es6 index 8f27724bb1..afd0df3618 100644 --- a/test/javascripts/acceptance/category-edit-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-test.js.es6 @@ -78,8 +78,8 @@ QUnit.test("Subcategory list settings", async assert => { ); await click(".edit-category-general"); - await categoryChooser.expandAwait(); - await categoryChooser.selectRowByValueAwait(3); + await categoryChooser.expand(); + await categoryChooser.selectRowByValue(3); await click(".edit-category-settings a"); diff --git a/test/javascripts/acceptance/composer-actions-test.js.es6 b/test/javascripts/acceptance/composer-actions-test.js.es6 index 11517d60d3..d6b2bfd127 100644 --- a/test/javascripts/acceptance/composer-actions-test.js.es6 +++ b/test/javascripts/acceptance/composer-actions-test.js.es6 @@ -16,7 +16,7 @@ QUnit.test("replying to post", async assert => { await visit("/t/internationalization-localization/280"); await click("article#post_3 button.reply"); - await composerActions.expandAwait(); + await composerActions.expand(); assert.equal(composerActions.rowByIndex(0).value(), "reply_as_new_topic"); assert.equal( @@ -34,8 +34,8 @@ QUnit.test("replying to post - reply_as_private_message", async assert => { await visit("/t/internationalization-localization/280"); await click("article#post_3 button.reply"); - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait("reply_as_private_message"); + await composerActions.expand(); + await composerActions.selectRowByValue("reply_as_private_message"); assert.equal(find(".users-input .item:eq(0)").text(), "codinghorror"); assert.ok( @@ -55,8 +55,8 @@ QUnit.test("replying to post - reply_to_topic", async assert => { "test replying to topic when initially replied to post" ); - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait("reply_to_topic"); + await composerActions.expand(); + await composerActions.selectRowByValue("reply_to_topic"); assert.equal( find(".action-title .topic-link") @@ -84,8 +84,8 @@ QUnit.test("replying to post - toggle_whisper", async assert => { "test replying as whisper to topic when initially not a whisper" ); - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait("toggle_whisper"); + await composerActions.expand(); + await composerActions.selectRowByValue("toggle_whisper"); assert.ok( find(".composer-fields .whisper") @@ -103,15 +103,15 @@ QUnit.test("replying to post - reply_as_new_topic", async assert => { await visit("/t/internationalization-localization/280"); await click("#topic-title .d-icon-pencil"); - await categoryChooser.expandAwait(); - await categoryChooser.selectRowByValueAwait(4); + await categoryChooser.expand(); + await categoryChooser.selectRowByValue(4); await click("#topic-title .submit-edit"); await click("article#post_3 button.reply"); await fillIn(".d-editor-input", quote); - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait("reply_as_new_topic"); + await composerActions.expand(); + await composerActions.selectRowByValue("reply_as_new_topic"); assert.equal(categoryChooserReplyArea.header().name(), "faq"); assert.equal( @@ -133,8 +133,8 @@ QUnit.test("shared draft", async assert => { await visit("/"); await click("#create-topic"); - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait("shared_draft"); + await composerActions.expand(); + await composerActions.selectRowByValue("shared_draft"); assert.equal( find("#reply-control .btn-primary.create .d-button-label").text(), @@ -159,8 +159,8 @@ QUnit.test("interactions", async assert => { await visit("/t/internationalization-localization/280"); await click("article#post_3 button.reply"); await fillIn(".d-editor-input", quote); - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait("reply_to_topic"); + await composerActions.expand(); + await composerActions.selectRowByValue("reply_to_topic"); assert.equal( find(".action-title") @@ -170,7 +170,7 @@ QUnit.test("interactions", async assert => { ); assert.equal(find(".d-editor-input").val(), quote); - await composerActions.expandAwait(); + await composerActions.expand(); assert.equal(composerActions.rowByIndex(0).value(), "reply_as_new_topic"); assert.equal(composerActions.rowByIndex(1).value(), "reply_to_post"); @@ -181,8 +181,8 @@ QUnit.test("interactions", async assert => { assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); assert.equal(composerActions.rows().length, 4); - await composerActions.selectRowByValueAwait("reply_to_post"); - await composerActions.expandAwait(); + await composerActions.selectRowByValue("reply_to_post"); + await composerActions.expand(); assert.ok(exists(find(".action-title img.avatar"))); assert.equal( @@ -201,8 +201,8 @@ QUnit.test("interactions", async assert => { assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); assert.equal(composerActions.rows().length, 4); - await composerActions.selectRowByValueAwait("reply_as_new_topic"); - await composerActions.expandAwait(); + await composerActions.selectRowByValue("reply_as_new_topic"); + await composerActions.expand(); assert.equal( find(".action-title") @@ -224,8 +224,8 @@ QUnit.test("interactions", async assert => { assert.equal(composerActions.rowByIndex(3).value(), "shared_draft"); assert.equal(composerActions.rows().length, 4); - await composerActions.selectRowByValueAwait("reply_as_private_message"); - await composerActions.expandAwait(); + await composerActions.selectRowByValue("reply_as_private_message"); + await composerActions.expand(); assert.equal( find(".action-title") diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 index 274d4e1642..ef25782bc6 100644 --- a/test/javascripts/acceptance/composer-test.js.es6 +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -295,8 +295,8 @@ QUnit.test( await visit("/t/this-is-a-test-topic/9"); await click(".topic-post:eq(0) button.reply"); - await selectKit(".toolbar-popup-menu-options").expandAwait(); - await selectKit(".toolbar-popup-menu-options").selectRowByValueAwait( + await selectKit(".toolbar-popup-menu-options").expand(); + await selectKit(".toolbar-popup-menu-options").selectRowByValue( "toggleWhisper" ); @@ -318,8 +318,8 @@ QUnit.test( "it should reset the state of the composer's model" ); - await selectKit(".toolbar-popup-menu-options").expandAwait(); - await selectKit(".toolbar-popup-menu-options").selectRowByValueAwait( + await selectKit(".toolbar-popup-menu-options").expand(); + await selectKit(".toolbar-popup-menu-options").selectRowByValue( "toggleInvisible" ); @@ -409,8 +409,8 @@ QUnit.test("Disable body until category is selected", async assert => { const categoryChooser = selectKit(".category-chooser"); - await categoryChooser.expandAwait(); - await categoryChooser.selectRowByValueAwait(2); + await categoryChooser.expand(); + await categoryChooser.selectRowByValue(2); assert.ok( find(".d-editor-textarea-wrapper.disabled").length === 0, @@ -418,8 +418,8 @@ QUnit.test("Disable body until category is selected", async assert => { ); await fillIn(".d-editor-input", "Now I can type stuff"); - await categoryChooser.expandAwait(); - await categoryChooser.selectRowByValueAwait("__none__"); + await categoryChooser.expand(); + await categoryChooser.selectRowByValue("__none__"); assert.ok( find(".d-editor-textarea-wrapper.disabled").length === 0, diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index 00df87be36..51a7eb0418 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -58,9 +58,12 @@ QUnit.test("update some fields", async assert => { await savePreferences(); click(".preferences-nav .nav-notifications a"); - selectKit(".control-group.notifications .combo-box.duration") - .expand() - .selectRowByValue(1440); + await selectKit( + ".control-group.notifications .combo-box.duration" + ).expand(); + await selectKit( + ".control-group.notifications .combo-box.duration" + ).selectRowByValue(1440); await savePreferences(); click(".preferences-nav .nav-categories a"); diff --git a/test/javascripts/acceptance/search-full-test.js.es6 b/test/javascripts/acceptance/search-full-test.js.es6 index a09f2c6cc2..fc6736146c 100644 --- a/test/javascripts/acceptance/search-full-test.js.es6 +++ b/test/javascripts/acceptance/search-full-test.js.es6 @@ -156,9 +156,9 @@ QUnit.test("update category through advanced search ui", async assert => { await fillIn(".search-query", "none"); - await categoryChooser.expandAwait(); + await categoryChooser.expand(); await categoryChooser.fillInFilter("faq"); - await categoryChooser.selectRowByValueAwait(4); + await categoryChooser.selectRowByValue(4); assert.ok( exists('.search-advanced-options .badge-category:contains("faq")'), @@ -317,8 +317,8 @@ QUnit.test("update in filter through advanced search ui", async assert => { await visit("/search"); await fillIn(".search-query", "none"); - await inSelector.expandAwait(); - await inSelector.selectRowByValueAwait("bookmarks"); + await inSelector.expand(); + await inSelector.selectRowByValue("bookmarks"); assert.ok( inSelector.rowByName("I bookmarked").exists(), @@ -339,8 +339,8 @@ QUnit.test("update status through advanced search ui", async assert => { await visit("/search"); await fillIn(".search-query", "none"); - await statusSelector.expandAwait(); - await statusSelector.selectRowByValueAwait("closed"); + await statusSelector.expand(); + await statusSelector.selectRowByValue("closed"); assert.ok( statusSelector.rowByName("are closed").exists(), @@ -362,8 +362,8 @@ QUnit.test("update post time through advanced search ui", async assert => { await fillIn(".search-query", "none"); await fillIn("#search-post-date .date-picker", "2016-10-05"); - await postTimeSelector.expandAwait(); - await postTimeSelector.selectRowByValueAwait("after"); + await postTimeSelector.expand(); + await postTimeSelector.selectRowByValue("after"); assert.ok( postTimeSelector.rowByName("after").exists(), diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 index cdc882d2ff..1cb01a1a88 100644 --- a/test/javascripts/acceptance/search-test.js.es6 +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -89,7 +89,7 @@ QUnit.test("Right filters are shown to anonymous users", async assert => { await visit("/search?expanded=true"); - await inSelector.expandAwait(); + await inSelector.expand(); assert.ok(inSelector.rowByValue("first").exists()); assert.ok(inSelector.rowByValue("pinned").exists()); @@ -115,7 +115,7 @@ QUnit.test("Right filters are shown to logged-in users", async assert => { Discourse.reset(); await visit("/search?expanded=true"); - await inSelector.expandAwait(); + await inSelector.expand(); assert.ok(inSelector.rowByValue("first").exists()); assert.ok(inSelector.rowByValue("pinned").exists()); diff --git a/test/javascripts/acceptance/topic-edit-timer-test.js.es6 b/test/javascripts/acceptance/topic-edit-timer-test.js.es6 index 3d969ba8c9..682d8bf149 100644 --- a/test/javascripts/acceptance/topic-edit-timer-test.js.es6 +++ b/test/javascripts/acceptance/topic-edit-timer-test.js.es6 @@ -51,8 +51,8 @@ QUnit.test("autoclose - specific time", async assert => { await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("next_week"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("next_week"); assert.equal(futureDateInputSelector.header().title(), "Next week"); assert.equal(futureDateInputSelector.header().value(), "next_week"); @@ -71,8 +71,8 @@ QUnit.test("autoclose", async assert => { await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("next_week"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("next_week"); assert.equal(futureDateInputSelector.header().title(), "Next week"); assert.equal(futureDateInputSelector.header().value(), "next_week"); @@ -83,8 +83,8 @@ QUnit.test("autoclose", async assert => { .trim(); assert.ok(regex1.test(html1)); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("pick_date_and_time"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("pick_date_and_time"); await fillIn(".future-date-input .date-picker", "2099-11-24"); @@ -97,8 +97,8 @@ QUnit.test("autoclose", async assert => { .trim(); assert.ok(regex2.test(html2)); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("set_based_on_last_post"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("set_based_on_last_post"); await fillIn(".future-date-input input[type=number]", "2"); @@ -126,14 +126,14 @@ QUnit.test("close temporarily", async assert => { await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await timerType.expandAwait(); - await timerType.selectRowByValueAwait("open"); + await timerType.expand(); + await timerType.selectRowByValue("open"); assert.equal(futureDateInputSelector.header().title(), "Select a timeframe"); assert.equal(futureDateInputSelector.header().value(), null); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("next_week"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("next_week"); assert.equal(futureDateInputSelector.header().title(), "Next week"); assert.equal(futureDateInputSelector.header().value(), "next_week"); @@ -144,8 +144,8 @@ QUnit.test("close temporarily", async assert => { .trim(); assert.ok(regex1.test(html1)); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("pick_date_and_time"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("pick_date_and_time"); await fillIn(".future-date-input .date-picker", "2099-11-24"); @@ -168,8 +168,8 @@ QUnit.test("schedule", async assert => { await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await timerType.expandAwait(); - await timerType.selectRowByValueAwait("publish_to_category"); + await timerType.expand(); + await timerType.selectRowByValue("publish_to_category"); assert.equal(categoryChooser.header().title(), "uncategorized"); assert.equal(categoryChooser.header().value(), null); @@ -177,11 +177,11 @@ QUnit.test("schedule", async assert => { assert.equal(futureDateInputSelector.header().title(), "Select a timeframe"); assert.equal(futureDateInputSelector.header().value(), null); - await categoryChooser.expandAwait(); - await categoryChooser.selectRowByValueAwait("7"); + await categoryChooser.expand(); + await categoryChooser.selectRowByValue("7"); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("next_week"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("next_week"); assert.equal(futureDateInputSelector.header().title(), "Next week"); assert.equal(futureDateInputSelector.header().value(), "next_week"); @@ -202,7 +202,7 @@ QUnit.test("TL4 can't auto-delete", async assert => { const timerType = selectKit(".select-kit.timer-type"); - await timerType.expandAwait(); + await timerType.expand(); assert.ok(!timerType.rowByValue("delete").exists()); }); @@ -215,14 +215,14 @@ QUnit.test("auto delete", async assert => { await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await timerType.expandAwait(); - await timerType.selectRowByValueAwait("delete"); + await timerType.expand(); + await timerType.selectRowByValue("delete"); assert.equal(futureDateInputSelector.header().title(), "Select a timeframe"); assert.equal(futureDateInputSelector.header().value(), null); - await futureDateInputSelector.expandAwait(); - await futureDateInputSelector.selectRowByValueAwait("two_weeks"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("two_weeks"); assert.equal(futureDateInputSelector.header().title(), "Two Weeks"); assert.equal(futureDateInputSelector.header().value(), "two_weeks"); @@ -242,7 +242,8 @@ QUnit.test( await visit("/t/internationalization-localization"); await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await futureDateInputSelector.expand().selectRowByValue("next_week"); + await futureDateInputSelector.expand(); + await futureDateInputSelector.selectRowByValue("next_week"); await click(".modal-footer button.btn-primary"); const regex = /will automatically close in/g; diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 index 87d8f239fb..90a41acd71 100644 --- a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -20,8 +20,8 @@ QUnit.test("Updating topic notification level", async assert => { "it should display the notification options button in the topic's footer" ); - await notificationOptions.expandAwait(); - await notificationOptions.selectRowByValueAwait("3"); + await notificationOptions.expand(); + await notificationOptions.selectRowByValue("3"); assert.equal( notificationOptions.selectedRow().name(), diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6 index 9f9473e60a..f8e8f9ab58 100644 --- a/test/javascripts/acceptance/topic-test.js.es6 +++ b/test/javascripts/acceptance/topic-test.js.es6 @@ -54,8 +54,8 @@ QUnit.test("Updating the topic title and category", async assert => { await click("#topic-title .d-icon-pencil"); await fillIn("#edit-title", "this is the new title"); - await categoryChooser.expandAwait(); - await categoryChooser.selectRowByValueAwait(4); + await categoryChooser.expand(); + await categoryChooser.selectRowByValue(4); await click("#topic-title .submit-edit"); assert.equal( diff --git a/test/javascripts/components/categories-admin-dropdown-test.js.es6 b/test/javascripts/components/categories-admin-dropdown-test.js.es6 index c25224d81a..7e10646b4c 100644 --- a/test/javascripts/components/categories-admin-dropdown-test.js.es6 +++ b/test/javascripts/components/categories-admin-dropdown-test.js.es6 @@ -10,7 +10,7 @@ componentTest("default", { assert.equal(subject.el().find(".d-icon-bars").length, 1); assert.equal(subject.el().find(".d-icon-caret-down").length, 1); - await subject.expandAwait(); + await subject.expand(); assert.equal(subject.rowByValue("create").name(), "New Category"); } diff --git a/test/javascripts/components/category-chooser-test.js.es6 b/test/javascripts/components/category-chooser-test.js.es6 index 434336b476..271b00aa69 100644 --- a/test/javascripts/components/category-chooser-test.js.es6 +++ b/test/javascripts/components/category-chooser-test.js.es6 @@ -30,7 +30,7 @@ componentTest("with excludeCategoryId", { template: "{{category-chooser excludeCategoryId=2}}", async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.notOk( this.get("subject") @@ -44,7 +44,7 @@ componentTest("with scopedCategoryId", { template: "{{category-chooser scopedCategoryId=2}}", async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") diff --git a/test/javascripts/components/category-selector-test.js.es6 b/test/javascripts/components/category-selector-test.js.es6 index d4f158d1b0..88f4346db1 100644 --- a/test/javascripts/components/category-selector-test.js.es6 +++ b/test/javascripts/components/category-selector-test.js.es6 @@ -16,20 +16,18 @@ componentTest("default", { }, test(assert) { - andThen(() => { - assert.equal( - this.get("subject") - .header() - .value(), - 2 - ); - assert.notOk( - this.get("subject") - .rowByValue(2) - .exists(), - "selected categories are not in the list" - ); - }); + assert.equal( + this.get("subject") + .header() + .value(), + 2 + ); + assert.notOk( + this.get("subject") + .rowByValue(2) + .exists(), + "selected categories are not in the list" + ); } }); @@ -41,23 +39,21 @@ componentTest("with blacklist", { this.set("blacklist", [Category.findById(8)]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.ok( - this.get("subject") - .rowByValue(6) - .exists(), - "not blacklisted categories are in the list" - ); - assert.notOk( - this.get("subject") - .rowByValue(8) - .exists(), - "blacklisted categories are not in the list" - ); - }); + assert.ok( + this.get("subject") + .rowByValue(6) + .exists(), + "not blacklisted categories are in the list" + ); + assert.notOk( + this.get("subject") + .rowByValue(8) + .exists(), + "blacklisted categories are not in the list" + ); } }); @@ -68,39 +64,31 @@ componentTest("interactions", { this.set("categories", [Category.findById(2), Category.findById(6)]); }, - test(assert) { - this.get("subject") - .expand() - .selectRowByValue(8); + async test(assert) { + await this.get("subject").expand(); + await this.get("subject").selectRowByValue(8); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .value(), - "2,6,8", - "it adds the selected category" - ); - assert.equal(this.get("categories").length, 3); - }); + assert.equal( + this.get("subject") + .header() + .value(), + "2,6,8", + "it adds the selected category" + ); + assert.equal(this.get("categories").length, 3); - this.get("subject").expand(); - this.get("subject") - .keyboard() - .backspace(); - this.get("subject") - .keyboard() - .backspace(); + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .value(), - "2,6", - "it removes the last selected category" - ); - assert.equal(this.get("categories").length, 2); - }); + await this.get("subject").keyboard("backspace"); + await this.get("subject").keyboard("backspace"); + + assert.equal( + this.get("subject") + .header() + .value(), + "2,6", + "it removes the last selected category" + ); + assert.equal(this.get("categories").length, 2); } }); diff --git a/test/javascripts/components/combo-box-test.js.es6 b/test/javascripts/components/combo-box-test.js.es6 index c127157b90..af7ad93f30 100644 --- a/test/javascripts/components/combo-box-test.js.es6 +++ b/test/javascripts/components/combo-box-test.js.es6 @@ -13,7 +13,7 @@ componentTest("default", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -46,7 +46,7 @@ componentTest("with valueAttribute", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -70,7 +70,7 @@ componentTest("with nameProperty", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -94,7 +94,7 @@ componentTest("with an array as content", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -120,7 +120,7 @@ componentTest("with value and none as a string", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -148,7 +148,7 @@ componentTest("with value and none as a string", { ); assert.equal(this.get("value"), "trout"); - await this.get("subject").selectNoneRowAwait(); + await this.get("subject").selectNoneRow(); assert.equal(this.get("value"), null); } @@ -191,7 +191,7 @@ componentTest("with value and none as an object", { ); assert.equal(this.get("value"), "evil"); - await this.get("subject").selectNoneRowAwait(); + await this.get("subject").selectNoneRow(); assert.equal(this.get("value"), null); } @@ -207,7 +207,7 @@ componentTest("with no value and none as an object", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -228,7 +228,7 @@ componentTest("with no value and none string", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -247,7 +247,7 @@ componentTest("with no value and no none", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -289,7 +289,7 @@ componentTest("with noneLabel", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") diff --git a/test/javascripts/components/d-editor-test.js.es6 b/test/javascripts/components/d-editor-test.js.es6 index 58e4f88b30..19dedaf2bc 100644 --- a/test/javascripts/components/d-editor-test.js.es6 +++ b/test/javascripts/components/d-editor-test.js.es6 @@ -6,35 +6,31 @@ moduleForComponent("d-editor", { integration: true }); componentTest("preview updates with markdown", { template: "{{d-editor value=value}}", - test(assert) { + async test(assert) { assert.ok(this.$(".d-editor-button-bar").length); - fillIn(".d-editor-input", "hello **world**"); + await fillIn(".d-editor-input", "hello **world**"); - andThen(() => { - assert.equal(this.get("value"), "hello **world**"); - assert.equal( - this.$(".d-editor-preview") - .html() - .trim(), - "

hello world

" - ); - }); + assert.equal(this.get("value"), "hello **world**"); + assert.equal( + this.$(".d-editor-preview") + .html() + .trim(), + "

hello world

" + ); } }); componentTest("preview sanitizes HTML", { template: "{{d-editor value=value}}", - test(assert) { - fillIn(".d-editor-input", `">`); - andThen(() => { - assert.equal( - this.$(".d-editor-preview") - .html() - .trim(), - '

">

' - ); - }); + async test(assert) { + await fillIn(".d-editor-input", `">`); + assert.equal( + this.$(".d-editor-preview") + .html() + .trim(), + '

">

' + ); } }); @@ -45,7 +41,7 @@ componentTest("updating the value refreshes the preview", { this.set("value", "evil trout"); }, - test(assert) { + async test(assert) { assert.equal( this.$(".d-editor-preview") .html() @@ -53,14 +49,12 @@ componentTest("updating the value refreshes the preview", { "

evil trout

" ); - andThen(() => this.set("value", "zogstrip")); - andThen(() => - assert.equal( - this.$(".d-editor-preview") - .html() - .trim(), - "

zogstrip

" - ) + await this.set("value", "zogstrip"); + assert.equal( + this.$(".d-editor-preview") + .html() + .trim(), + "

zogstrip

" ); } }); @@ -97,216 +91,167 @@ function composerTestCase(title, testFunc) { }); } -testCase(`selecting the space before a word`, function(assert, textarea) { +testCase(`selecting the space before a word`, async function(assert, textarea) { textarea.selectionStart = 5; textarea.selectionEnd = 7; - click(`button.bold`); - andThen(() => { - assert.equal(this.get("value"), `hello **w**orld.`); - assert.equal(textarea.selectionStart, 8); - assert.equal(textarea.selectionEnd, 9); - }); + await click(`button.bold`); + + assert.equal(this.get("value"), `hello **w**orld.`); + assert.equal(textarea.selectionStart, 8); + assert.equal(textarea.selectionEnd, 9); }); -testCase(`selecting the space after a word`, function(assert, textarea) { +testCase(`selecting the space after a word`, async function(assert, textarea) { textarea.selectionStart = 0; textarea.selectionEnd = 6; - click(`button.bold`); - andThen(() => { - assert.equal(this.get("value"), `**hello** world.`); - assert.equal(textarea.selectionStart, 2); - assert.equal(textarea.selectionEnd, 7); - }); + await click(`button.bold`); + + assert.equal(this.get("value"), `**hello** world.`); + assert.equal(textarea.selectionStart, 2); + assert.equal(textarea.selectionEnd, 7); }); -testCase(`bold button with no selection`, function(assert, textarea) { - click(`button.bold`); - andThen(() => { - const example = I18n.t(`composer.bold_text`); - assert.equal(this.get("value"), `hello world.**${example}**`); - assert.equal(textarea.selectionStart, 14); - assert.equal(textarea.selectionEnd, 14 + example.length); - }); +testCase(`bold button with no selection`, async function(assert, textarea) { + await click(`button.bold`); + + const example = I18n.t(`composer.bold_text`); + assert.equal(this.get("value"), `hello world.**${example}**`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 14 + example.length); }); -testCase(`bold button with a selection`, function(assert, textarea) { +testCase(`bold button with a selection`, async function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; - click(`button.bold`); - andThen(() => { - assert.equal(this.get("value"), `hello **world**.`); - assert.equal(textarea.selectionStart, 8); - assert.equal(textarea.selectionEnd, 13); - }); + await click(`button.bold`); + assert.equal(this.get("value"), `hello **world**.`); + assert.equal(textarea.selectionStart, 8); + assert.equal(textarea.selectionEnd, 13); - click(`button.bold`); - andThen(() => { - assert.equal(this.get("value"), "hello world."); - assert.equal(textarea.selectionStart, 6); - assert.equal(textarea.selectionEnd, 11); - }); + await click(`button.bold`); + assert.equal(this.get("value"), "hello world."); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 11); }); -testCase(`bold with a multiline selection`, function(assert, textarea) { +testCase(`bold with a multiline selection`, async function(assert, textarea) { this.set("value", "hello\n\nworld\n\ntest."); - andThen(() => { - textarea.selectionStart = 0; - textarea.selectionEnd = 12; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 12; - click(`button.bold`); - andThen(() => { - assert.equal(this.get("value"), `**hello**\n\n**world**\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 20); - }); + await click(`button.bold`); + assert.equal(this.get("value"), `**hello**\n\n**world**\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 20); - click(`button.bold`); - andThen(() => { - assert.equal(this.get("value"), `hello\n\nworld\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 12); - }); + await click(`button.bold`); + assert.equal(this.get("value"), `hello\n\nworld\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 12); }); -testCase(`italic button with no selection`, function(assert, textarea) { - click(`button.italic`); - andThen(() => { - const example = I18n.t(`composer.italic_text`); - assert.equal(this.get("value"), `hello world._${example}_`); +testCase(`italic button with no selection`, async function(assert, textarea) { + await click(`button.italic`); + const example = I18n.t(`composer.italic_text`); + assert.equal(this.get("value"), `hello world._${example}_`); - assert.equal(textarea.selectionStart, 13); - assert.equal(textarea.selectionEnd, 13 + example.length); - }); + assert.equal(textarea.selectionStart, 13); + assert.equal(textarea.selectionEnd, 13 + example.length); }); -testCase(`italic button with a selection`, function(assert, textarea) { +testCase(`italic button with a selection`, async function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; - click(`button.italic`); - andThen(() => { - assert.equal(this.get("value"), `hello _world_.`); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 12); - }); + await click(`button.italic`); + assert.equal(this.get("value"), `hello _world_.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 12); - click(`button.italic`); - andThen(() => { - assert.equal(this.get("value"), "hello world."); - assert.equal(textarea.selectionStart, 6); - assert.equal(textarea.selectionEnd, 11); - }); + await click(`button.italic`); + assert.equal(this.get("value"), "hello world."); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 11); }); -testCase(`italic with a multiline selection`, function(assert, textarea) { +testCase(`italic with a multiline selection`, async function(assert, textarea) { this.set("value", "hello\n\nworld\n\ntest."); - andThen(() => { - textarea.selectionStart = 0; - textarea.selectionEnd = 12; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 12; - click(`button.italic`); - andThen(() => { - assert.equal(this.get("value"), `_hello_\n\n_world_\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 16); - }); + await click(`button.italic`); + assert.equal(this.get("value"), `_hello_\n\n_world_\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 16); - click(`button.italic`); - andThen(() => { - assert.equal(this.get("value"), `hello\n\nworld\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 12); - }); + await click(`button.italic`); + assert.equal(this.get("value"), `hello\n\nworld\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 12); }); -testCase("link modal (cancel)", function(assert) { +testCase("link modal (cancel)", async function(assert) { assert.equal(this.$(".insert-link.hidden").length, 1); - click("button.link"); - andThen(() => { - assert.equal(this.$(".insert-link.hidden").length, 0); - }); - - click(".insert-link button.btn-danger"); - andThen(() => { - assert.equal(this.$(".insert-link.hidden").length, 1); - assert.equal(this.get("value"), "hello world."); - }); -}); - -testCase("link modal (cancel clears inputs)", async function(assert) { await click("button.link"); - await fillIn(".insert-link input.link-url", "https://meta.discourse.org/"); - await fillIn(".insert-link input.link-text", "Discourse Meta"); + assert.equal(this.$(".insert-link.hidden").length, 0); + await click(".insert-link button.btn-danger"); - - await click("button.link"); - assert.equal(this.$(".insert-link input.link-url")[0].value, ""); - assert.equal(this.$(".insert-link input.link-text")[0].value, ""); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal(this.get("value"), "hello world."); }); -testCase("link modal (simple link)", function(assert, textarea) { - click("button.link"); +testCase("link modal (simple link)", async function(assert, textarea) { + await click("button.link"); const url = "http://eviltrout.com"; - fillIn(".insert-link input.link-url", url); - click(".insert-link button.btn-primary"); - andThen(() => { - assert.equal(this.$(".insert-link.hidden").length, 1); - assert.equal(this.get("value"), `hello world.[${url}](${url})`); - assert.equal(textarea.selectionStart, 13); - assert.equal(textarea.selectionEnd, 13 + url.length); - }); + await fillIn(".insert-link input.link-url", url); + await click(".insert-link button.btn-primary"); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal(this.get("value"), `hello world.[${url}](${url})`); + assert.equal(textarea.selectionStart, 13); + assert.equal(textarea.selectionEnd, 13 + url.length); }); -testCase("link modal auto http addition", function(assert) { - click("button.link"); - fillIn(".insert-link input.link-url", "sam.com"); - click(".insert-link button.btn-primary"); - andThen(() => { - assert.equal(this.get("value"), `hello world.[sam.com](http://sam.com)`); - }); +testCase("link modal auto http addition", async function(assert) { + await click("button.link"); + await fillIn(".insert-link input.link-url", "sam.com"); + await click(".insert-link button.btn-primary"); + assert.equal(this.get("value"), `hello world.[sam.com](http://sam.com)`); }); -testCase("link modal (simple link) with selected text", function( +testCase("link modal (simple link) with selected text", async function( assert, textarea ) { textarea.selectionStart = 0; textarea.selectionEnd = 12; - click("button.link"); - andThen(() => { - assert.equal(this.$("input.link-text")[0].value, "hello world."); - }); - fillIn(".insert-link input.link-url", "http://eviltrout.com"); - click(".insert-link button.btn-primary"); - andThen(() => { - assert.equal(this.$(".insert-link.hidden").length, 1); - assert.equal(this.get("value"), "[hello world.](http://eviltrout.com)"); - }); + await click("button.link"); + assert.equal(this.$("input.link-text")[0].value, "hello world."); + + await fillIn(".insert-link input.link-url", "http://eviltrout.com"); + await click(".insert-link button.btn-primary"); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal(this.get("value"), "[hello world.](http://eviltrout.com)"); }); -testCase("link modal (link with description)", function(assert) { - click("button.link"); - fillIn(".insert-link input.link-url", "http://eviltrout.com"); - fillIn(".insert-link input.link-text", "evil trout"); - click(".insert-link button.btn-primary"); - andThen(() => { - assert.equal(this.$(".insert-link.hidden").length, 1); - assert.equal( - this.get("value"), - "hello world.[evil trout](http://eviltrout.com)" - ); - }); +testCase("link modal (link with description)", async function(assert) { + await click("button.link"); + await fillIn(".insert-link input.link-url", "http://eviltrout.com"); + await fillIn(".insert-link input.link-text", "evil trout"); + await click(".insert-link button.btn-primary"); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal( + this.get("value"), + "hello world.[evil trout](http://eviltrout.com)" + ); }); componentTest("advanced code", { @@ -325,24 +270,22 @@ function xyz(x, y, z) { ); }, - test(assert) { + async test(assert) { const textarea = this.$("textarea.d-editor-input")[0]; textarea.selectionStart = 0; textarea.selectionEnd = textarea.value.length; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - ` + await click("button.code"); + assert.equal( + this.get("value"), + ` function xyz(x, y, z) { if (y === z) { return true; } } ` - ); - }); + ); } }); @@ -352,111 +295,85 @@ componentTest("code button", { this.siteSettings.code_formatting_style = "4-spaces-indent"; }, - test(assert) { + async test(assert) { const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); - click("button.code"); - andThen(() => { - assert.equal(this.get("value"), ` ${I18n.t("composer.code_text")}`); + await click("button.code"); + assert.equal(this.get("value"), ` ${I18n.t("composer.code_text")}`); - this.set("value", "first line\n\nsecond line\n\nthird line"); + this.set("value", "first line\n\nsecond line\n\nthird line"); - textarea.selectionStart = 11; - textarea.selectionEnd = 11; - }); + textarea.selectionStart = 11; + textarea.selectionEnd = 11; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `first line + await click("button.code"); + assert.equal( + this.get("value"), + `first line ${I18n.t("composer.code_text")} second line third line` - ); + ); - this.set("value", "first line\n\nsecond line\n\nthird line"); - }); + this.set("value", "first line\n\nsecond line\n\nthird line"); - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `first line + await click("button.code"); + assert.equal( + this.get("value"), + `first line second line third line\`${I18n.t("composer.code_title")}\`` - ); - this.set("value", "first line\n\nsecond line\n\nthird line"); - }); + ); + this.set("value", "first line\n\nsecond line\n\nthird line"); - andThen(() => { - textarea.selectionStart = 5; - textarea.selectionEnd = 5; - }); + textarea.selectionStart = 5; + textarea.selectionEnd = 5; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `first\`${I18n.t("composer.code_title")}\` line + await click("button.code"); + assert.equal( + this.get("value"), + `first\`${I18n.t("composer.code_title")}\` line second line third line` - ); - this.set("value", "first line\n\nsecond line\n\nthird line"); - }); + ); + this.set("value", "first line\n\nsecond line\n\nthird line"); - andThen(() => { - textarea.selectionStart = 6; - textarea.selectionEnd = 10; - }); + textarea.selectionStart = 6; + textarea.selectionEnd = 10; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - "first `line`\n\nsecond line\n\nthird line" - ); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 11); - }); + await click("button.code"); + assert.equal( + this.get("value"), + "first `line`\n\nsecond line\n\nthird line" + ); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 11); - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - "first line\n\nsecond line\n\nthird line" - ); - assert.equal(textarea.selectionStart, 6); - assert.equal(textarea.selectionEnd, 10); + await click("button.code"); + assert.equal(this.get("value"), "first line\n\nsecond line\n\nthird line"); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 10); - textarea.selectionStart = 0; - textarea.selectionEnd = 23; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 23; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - " first line\n\n second line\n\nthird line" - ); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 31); - }); + await click("button.code"); + assert.equal( + this.get("value"), + " first line\n\n second line\n\nthird line" + ); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 31); - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - "first line\n\nsecond line\n\nthird line" - ); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 23); - }); + await click("button.code"); + assert.equal(this.get("value"), "first line\n\nsecond line\n\nthird line"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 23); } }); @@ -466,117 +383,110 @@ componentTest("code fences", { this.set("value", ""); }, - test(assert) { + async test(assert) { const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `\`\`\` + await click("button.code"); + assert.equal( + this.get("value"), + `\`\`\` ${I18n.t("composer.paste_code_text")} \`\`\`` - ); + ); - assert.equal(textarea.selectionStart, 4); - assert.equal(textarea.selectionEnd, 27); + assert.equal(textarea.selectionStart, 4); + assert.equal(textarea.selectionEnd, 27); - this.set("value", "first line\nsecond line\nthird line"); + this.set("value", "first line\nsecond line\nthird line"); - textarea.selectionStart = 0; - textarea.selectionEnd = textarea.value.length; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `\`\`\` + await click("button.code"); + + assert.equal( + this.get("value"), + `\`\`\` first line second line third line \`\`\` ` - ); + ); - assert.equal(textarea.selectionStart, textarea.value.length); - assert.equal(textarea.selectionEnd, textarea.value.length); + assert.equal(textarea.selectionStart, textarea.value.length); + assert.equal(textarea.selectionEnd, textarea.value.length); - this.set("value", "first line\nsecond line\nthird line"); + this.set("value", "first line\nsecond line\nthird line"); - textarea.selectionStart = 0; - textarea.selectionEnd = 0; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 0; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `\`${I18n.t("composer.code_title")}\`first line + await click("button.code"); + + assert.equal( + this.get("value"), + `\`${I18n.t("composer.code_title")}\`first line second line third line` - ); + ); - assert.equal(textarea.selectionStart, 1); - assert.equal( - textarea.selectionEnd, - I18n.t("composer.code_title").length + 1 - ); + assert.equal(textarea.selectionStart, 1); + assert.equal( + textarea.selectionEnd, + I18n.t("composer.code_title").length + 1 + ); - this.set("value", "first line\nsecond line\nthird line"); + this.set("value", "first line\nsecond line\nthird line"); - textarea.selectionStart = 0; - textarea.selectionEnd = 10; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 10; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `\`first line\` + await click("button.code"); + + assert.equal( + this.get("value"), + `\`first line\` second line third line` - ); + ); - assert.equal(textarea.selectionStart, 1); - assert.equal(textarea.selectionEnd, 11); + assert.equal(textarea.selectionStart, 1); + assert.equal(textarea.selectionEnd, 11); - this.set("value", "first line\nsecond line\nthird line"); + this.set("value", "first line\nsecond line\nthird line"); - textarea.selectionStart = 0; - textarea.selectionEnd = 23; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 23; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `\`\`\` + await click("button.code"); + + assert.equal( + this.get("value"), + `\`\`\` first line second line \`\`\` third line` - ); + ); - assert.equal(textarea.selectionStart, 30); - assert.equal(textarea.selectionEnd, 30); + assert.equal(textarea.selectionStart, 30); + assert.equal(textarea.selectionEnd, 30); - this.set("value", "first line\nsecond line\nthird line"); + this.set("value", "first line\nsecond line\nthird line"); - textarea.selectionStart = 6; - textarea.selectionEnd = 17; - }); + textarea.selectionStart = 6; + textarea.selectionEnd = 17; - click("button.code"); - andThen(() => { - assert.equal( - this.get("value"), - `first \n\`\`\`\nline\nsecond\n\`\`\`\n line\nthird line` - ); + await click("button.code"); - assert.equal(textarea.selectionStart, 27); - assert.equal(textarea.selectionEnd, 27); - }); + assert.equal( + this.get("value"), + `first \n\`\`\`\nline\nsecond\n\`\`\`\n line\nthird line` + ); + + assert.equal(textarea.selectionStart, 27); + assert.equal(textarea.selectionEnd, 27); } }); @@ -585,24 +495,19 @@ componentTest("quote button - empty lines", { beforeEach() { this.set("value", "one\n\ntwo\n\nthree"); }, - test(assert) { + async test(assert) { const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); - andThen(() => { - textarea.selectionStart = 0; - }); + textarea.selectionStart = 0; - click("button.quote"); - andThen(() => { - assert.equal(this.get("value"), "> one\n> \n> two\n> \n> three"); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 25); - }); + await click("button.quote"); - click("button.quote"); - andThen(() => { - assert.equal(this.get("value"), "one\n\ntwo\n\nthree"); - }); + assert.equal(this.get("value"), "> one\n> \n> two\n> \n> three"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 25); + + await click("button.quote"); + assert.equal(this.get("value"), "one\n\ntwo\n\nthree"); } }); @@ -611,172 +516,130 @@ componentTest("quote button - selecting empty lines", { beforeEach() { this.set("value", "one\n\n\n\ntwo"); }, - test(assert) { + async test(assert) { const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); - andThen(() => { - textarea.selectionStart = 6; - textarea.selectionEnd = 10; - }); + textarea.selectionStart = 6; + textarea.selectionEnd = 10; - click("button.quote"); - andThen(() => { - assert.equal(this.get("value"), "one\n\n\n> \n> two"); - }); + await click("button.quote"); + assert.equal(this.get("value"), "one\n\n\n> \n> two"); } }); -testCase("quote button", function(assert, textarea) { - andThen(() => { - textarea.selectionStart = 6; - textarea.selectionEnd = 9; - }); +testCase("quote button", async function(assert, textarea) { + textarea.selectionStart = 6; + textarea.selectionEnd = 9; - click("button.quote"); - andThen(() => { - assert.equal(this.get("value"), "hello\n\n> wor\n\nld."); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 12); - }); + await click("button.quote"); + assert.equal(this.get("value"), "hello\n\n> wor\n\nld."); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 12); - click("button.quote"); + await click("button.quote"); - andThen(() => { - assert.equal(this.get("value"), "hello\n\nwor\n\nld."); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 10); - }); + assert.equal(this.get("value"), "hello\n\nwor\n\nld."); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 10); - andThen(() => { - textarea.selectionStart = 15; - textarea.selectionEnd = 15; - }); + textarea.selectionStart = 15; + textarea.selectionEnd = 15; - click("button.quote"); - andThen(() => { - assert.equal(this.get("value"), "hello\n\nwor\n\nld.\n\n> Blockquote"); - }); + await click("button.quote"); + assert.equal(this.get("value"), "hello\n\nwor\n\nld.\n\n> Blockquote"); }); -testCase(`bullet button with no selection`, function(assert, textarea) { +testCase(`bullet button with no selection`, async function(assert, textarea) { const example = I18n.t("composer.list_item"); - click(`button.bullet`); - andThen(() => { - assert.equal(this.get("value"), `hello world.\n\n* ${example}`); - assert.equal(textarea.selectionStart, 14); - assert.equal(textarea.selectionEnd, 16 + example.length); - }); + await click(`button.bullet`); + assert.equal(this.get("value"), `hello world.\n\n* ${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 16 + example.length); - click(`button.bullet`); - andThen(() => { - assert.equal(this.get("value"), `hello world.\n\n${example}`); - }); + await click(`button.bullet`); + assert.equal(this.get("value"), `hello world.\n\n${example}`); }); -testCase(`bullet button with a selection`, function(assert, textarea) { +testCase(`bullet button with a selection`, async function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; - click(`button.bullet`); - andThen(() => { - assert.equal(this.get("value"), `hello\n\n* world\n\n.`); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 14); - }); + await click(`button.bullet`); + assert.equal(this.get("value"), `hello\n\n* world\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 14); - click(`button.bullet`); - andThen(() => { - assert.equal(this.get("value"), `hello\n\nworld\n\n.`); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 12); - }); + await click(`button.bullet`); + assert.equal(this.get("value"), `hello\n\nworld\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 12); }); -testCase(`bullet button with a multiple line selection`, function( +testCase(`bullet button with a multiple line selection`, async function( assert, textarea ) { this.set("value", "* Hello\n\nWorld\n\nEvil"); - andThen(() => { - textarea.selectionStart = 0; - textarea.selectionEnd = 20; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 20; - click(`button.bullet`); - andThen(() => { - assert.equal(this.get("value"), "Hello\n\nWorld\n\nEvil"); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 18); - }); + await click(`button.bullet`); + assert.equal(this.get("value"), "Hello\n\nWorld\n\nEvil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 18); - click(`button.bullet`); - andThen(() => { - assert.equal(this.get("value"), "* Hello\n\n* World\n\n* Evil"); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 24); - }); + await click(`button.bullet`); + assert.equal(this.get("value"), "* Hello\n\n* World\n\n* Evil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 24); }); -testCase(`list button with no selection`, function(assert, textarea) { +testCase(`list button with no selection`, async function(assert, textarea) { const example = I18n.t("composer.list_item"); - click(`button.list`); - andThen(() => { - assert.equal(this.get("value"), `hello world.\n\n1. ${example}`); - assert.equal(textarea.selectionStart, 14); - assert.equal(textarea.selectionEnd, 17 + example.length); - }); + await click(`button.list`); + assert.equal(this.get("value"), `hello world.\n\n1. ${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 17 + example.length); - click(`button.list`); - andThen(() => { - assert.equal(this.get("value"), `hello world.\n\n${example}`); - assert.equal(textarea.selectionStart, 14); - assert.equal(textarea.selectionEnd, 14 + example.length); - }); + await click(`button.list`); + assert.equal(this.get("value"), `hello world.\n\n${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 14 + example.length); }); -testCase(`list button with a selection`, function(assert, textarea) { +testCase(`list button with a selection`, async function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; - click(`button.list`); - andThen(() => { - assert.equal(this.get("value"), `hello\n\n1. world\n\n.`); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 15); - }); + await click(`button.list`); + assert.equal(this.get("value"), `hello\n\n1. world\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 15); - click(`button.list`); - andThen(() => { - assert.equal(this.get("value"), `hello\n\nworld\n\n.`); - assert.equal(textarea.selectionStart, 7); - assert.equal(textarea.selectionEnd, 12); - }); + await click(`button.list`); + assert.equal(this.get("value"), `hello\n\nworld\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 12); }); -testCase(`list button with line sequence`, function(assert, textarea) { +testCase(`list button with line sequence`, async function(assert, textarea) { this.set("value", "Hello\n\nWorld\n\nEvil"); - andThen(() => { - textarea.selectionStart = 0; - textarea.selectionEnd = 18; - }); + textarea.selectionStart = 0; + textarea.selectionEnd = 18; - click(`button.list`); - andThen(() => { - assert.equal(this.get("value"), "1. Hello\n\n2. World\n\n3. Evil"); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 27); - }); + await click(`button.list`); + assert.equal(this.get("value"), "1. Hello\n\n2. World\n\n3. Evil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 27); - click(`button.list`); - andThen(() => { - assert.equal(this.get("value"), "Hello\n\nWorld\n\nEvil"); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 18); - }); + await click(`button.list`); + assert.equal(this.get("value"), "Hello\n\nWorld\n\nEvil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 18); }); componentTest("clicking the toggle-direction button toggles the direction", { @@ -786,36 +649,31 @@ componentTest("clicking the toggle-direction button toggles the direction", { this.siteSettings.default_locale = "en"; }, - test(assert) { + async test(assert) { const textarea = this.$("textarea.d-editor-input"); - click("button.toggle-direction"); - andThen(() => { - assert.equal(textarea.attr("dir"), "rtl"); - }); - click("button.toggle-direction"); - andThen(() => { - assert.equal(textarea.attr("dir"), "ltr"); - }); + await click("button.toggle-direction"); + assert.equal(textarea.attr("dir"), "rtl"); + await click("button.toggle-direction"); + assert.equal(textarea.attr("dir"), "ltr"); } }); -testCase(`doesn't jump to bottom with long text`, function(assert, textarea) { +testCase(`doesn't jump to bottom with long text`, async function( + assert, + textarea +) { let longText = "hello world."; for (let i = 0; i < 8; i++) { longText = longText + longText; } this.set("value", longText); - andThen(() => { - $(textarea).scrollTop(0); - textarea.selectionStart = 3; - textarea.selectionEnd = 3; - }); + $(textarea).scrollTop(0); + textarea.selectionStart = 3; + textarea.selectionEnd = 3; - click("button.bold"); - andThen(() => { - assert.equal($(textarea).scrollTop(), 0, "it stays scrolled up"); - }); + await click("button.bold"); + assert.equal($(textarea).scrollTop(), 0, "it stays scrolled up"); }); componentTest("emoji", { @@ -834,45 +692,35 @@ componentTest("emoji", { }); this.set("value", "hello world."); }, - test(assert) { + async test(assert) { jumpEnd(this.$("textarea.d-editor-input")[0]); - click("button.emoji"); + await click("button.emoji"); - click( + await click( '.emoji-picker .section[data-section="people"] button.emoji[title="grinning"]' ); - andThen(() => { - assert.equal(this.get("value"), "hello world.:grinning:"); - }); + assert.equal(this.get("value"), "hello world.:grinning:"); } }); -testCase("replace-text event by default", function(assert) { +testCase("replace-text event by default", async function(assert) { this.set("value", "red green blue"); - andThen(() => { - this.container - .lookup("app-events:main") - .trigger("composer:replace-text", "green", "yellow"); - }); + await this.container + .lookup("app-events:main") + .trigger("composer:replace-text", "green", "yellow"); - andThen(() => { - assert.equal(this.get("value"), "red green blue"); - }); + assert.equal(this.get("value"), "red green blue"); }); -composerTestCase("replace-text event for composer", function(assert) { +composerTestCase("replace-text event for composer", async function(assert) { this.set("value", "red green blue"); - andThen(() => { - this.container - .lookup("app-events:main") - .trigger("composer:replace-text", "green", "yellow"); - }); + await this.container + .lookup("app-events:main") + .trigger("composer:replace-text", "green", "yellow"); - andThen(() => { - assert.equal(this.get("value"), "red yellow blue"); - }); + assert.equal(this.get("value"), "red yellow blue"); }); (() => { @@ -949,39 +797,26 @@ composerTestCase("replace-text event for composer", function(assert) { return [start, end - start]; } - function formatTextWithSelection(text, [start, len]) { - return [ - '"', - text.substr(0, start), - "<", - text.substr(start, len), - ">", - text.substr(start + len), - '"' - ].join(""); - } - for (let i = 0; i < CASES.length; i++) { const CASE = CASES[i]; - composerTestCase(`replace-text event: ${CASE.description}`, function( + // prettier-ignore + composerTestCase(`replace-text event: ${CASE.description}`, async function( // eslint-disable-line no-loop-func assert, textarea ) { this.set("value", BEFORE); - setSelection(textarea, CASE.before); - andThen(() => { - this.container - .lookup("app-events:main") - .trigger("composer:replace-text", "green", "yellow"); - }); - andThen(() => { - let expect = formatTextWithSelection(AFTER, CASE.after); - let actual = formatTextWithSelection( - this.get("value"), - getSelection(textarea) - ); - assert.equal(actual, expect); - }); + await setSelection(textarea, CASE.before); + + this.container + .lookup("app-events:main") + .trigger("composer:replace-text", "green", "yellow"); + + let expect = await formatTextWithSelection(AFTER, CASE.after); // eslint-disable-line no-undef + let actual = await formatTextWithSelection( // eslint-disable-line no-undef + this.get("value"), + getSelection(textarea) + ); + assert.equal(actual, expect); }); } })(); diff --git a/test/javascripts/components/list-setting-test.js.es6 b/test/javascripts/components/list-setting-test.js.es6 index a69d17a8f9..2926c2d481 100644 --- a/test/javascripts/components/list-setting-test.js.es6 +++ b/test/javascripts/components/list-setting-test.js.es6 @@ -11,20 +11,18 @@ componentTest("default", { }, test(assert) { - andThen(() => { - assert.equal( - selectKit() - .header() - .title(), - "bold,italic" - ); - assert.equal( - selectKit() - .header() - .value(), - "bold,italic" - ); - }); + assert.equal( + selectKit() + .header() + .title(), + "bold,italic" + ); + assert.equal( + selectKit() + .header() + .value(), + "bold,italic" + ); } }); @@ -36,14 +34,12 @@ componentTest("with empty string as value", { }, test(assert) { - andThen(() => { - assert.equal( - selectKit() - .header() - .value(), - "" - ); - }); + assert.equal( + selectKit() + .header() + .value(), + "" + ); } }); @@ -55,14 +51,12 @@ componentTest("with only setting value", { }, test(assert) { - andThen(() => { - assert.equal( - selectKit() - .header() - .value(), - "bold,italic" - ); - }); + assert.equal( + selectKit() + .header() + .value(), + "bold,italic" + ); } }); @@ -74,35 +68,26 @@ componentTest("interactions", { this.set("choices", ["bold", "italic", "underline"]); }, - test(assert) { + async test(assert) { const listSetting = selectKit(); - listSetting.expand().selectRowByValue("underline"); + await listSetting.expand(); + await listSetting.selectRowByValue("underline"); - andThen(() => { - assert.equal(listSetting.header().value(), "bold,italic,underline"); - }); + assert.equal(listSetting.header().value(), "bold,italic,underline"); - listSetting.expand().fillInFilter("strike"); + await listSetting.expand(); + await listSetting.fillInFilter("strike"); - andThen(() => { - assert.equal(listSetting.highlightedRow().value(), "strike"); - }); + assert.equal(listSetting.highlightedRow().value(), "strike"); - listSetting.keyboard().enter(); + await listSetting.keyboard("enter"); - andThen(() => { - assert.equal( - listSetting.header().value(), - "bold,italic,underline,strike" - ); - }); + assert.equal(listSetting.header().value(), "bold,italic,underline,strike"); - listSetting.keyboard().backspace(); - listSetting.keyboard().backspace(); + await listSetting.keyboard("backspace"); + await listSetting.keyboard("backspace"); - andThen(() => { - assert.equal(listSetting.header().value(), "bold,italic,underline"); - }); + assert.equal(listSetting.header().value(), "bold,italic,underline"); } }); diff --git a/test/javascripts/components/multi-select-test.js.es6 b/test/javascripts/components/multi-select-test.js.es6 index cf37448d71..b1f78fdfae 100644 --- a/test/javascripts/components/multi-select-test.js.es6 +++ b/test/javascripts/components/multi-select-test.js.es6 @@ -15,14 +15,12 @@ componentTest("with objects and values", { }, test(assert) { - andThen(() => { - assert.equal( - this.get("subject") - .header() - .value(), - "1,2" - ); - }); + assert.equal( + this.get("subject") + .header() + .value(), + "1,2" + ); } }); @@ -34,13 +32,11 @@ componentTest("with title", { }, test(assert) { - andThen(() => - assert.equal( - selectKit() - .header() - .title(), - "My title" - ) + assert.equal( + selectKit() + .header() + .title(), + "My title" ); } }); @@ -58,165 +54,127 @@ componentTest("interactions", { this.set("values", [1, 2]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .highlightedRow() - .name(), - "robin", - "it highlights the first content row" - ); - }); + assert.equal( + this.get("subject") + .highlightedRow() + .name(), + "robin", + "it highlights the first content row" + ); - this.set("none", "test.none"); + await this.set("none", "test.none"); - andThen(() => { - assert.ok( - this.get("subject") - .noneRow() - .exists() - ); - assert.equal( - this.get("subject") - .highlightedRow() - .name(), - "robin", - "it highlights the first content row" - ); - }); + assert.ok( + this.get("subject") + .noneRow() + .exists() + ); + assert.equal( + this.get("subject") + .highlightedRow() + .name(), + "robin", + "it highlights the first content row" + ); - this.get("subject").selectRowByValue(3); - this.get("subject").expand(); + await this.get("subject").selectRowByValue(3); + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .highlightedRow() - .name(), - "none", - "it highlights none row if no content" - ); - }); + assert.equal( + this.get("subject") + .highlightedRow() + .name(), + "none", + "it highlights none row if no content" + ); - this.get("subject").fillInFilter("joffrey"); + await this.get("subject").fillInFilter("joffrey"); - andThen(() => { - assert.equal( - this.get("subject") - .highlightedRow() - .name(), - "joffrey", - "it highlights create row when filling filter" - ); - }); + assert.equal( + this.get("subject") + .highlightedRow() + .name(), + "joffrey", + "it highlights create row when filling filter" + ); - this.get("subject") - .keyboard() - .enter(); + await this.get("subject").keyboard("enter"); - andThen(() => { - assert.equal( - this.get("subject") - .highlightedRow() - .name(), - "none", - "it highlights none row after creating content and no content left" - ); - }); + assert.equal( + this.get("subject") + .highlightedRow() + .name(), + "none", + "it highlights none row after creating content and no content left" + ); - this.get("subject") - .keyboard() - .backspace(); + await this.get("subject").keyboard("backspace"); - andThen(() => { - const $lastSelectedName = this.get("subject") - .header() - .el() - .find(".selected-name") - .last(); - assert.equal($lastSelectedName.attr("data-name"), "joffrey"); - assert.ok( - $lastSelectedName.hasClass("is-highlighted"), - "it highlights the last selected name when using backspace" - ); - }); + const $lastSelectedName = this.get("subject") + .header() + .el() + .find(".selected-name") + .last(); + assert.equal($lastSelectedName.attr("data-name"), "joffrey"); + assert.ok( + $lastSelectedName.hasClass("is-highlighted"), + "it highlights the last selected name when using backspace" + ); - this.get("subject") - .keyboard() - .backspace(); + await this.get("subject").keyboard("backspace"); - andThen(() => { - const $lastSelectedName = this.get("subject") - .header() - .el() - .find(".selected-name") - .last(); - assert.equal( - $lastSelectedName.attr("data-name"), - "robin", - "it removes the previous highlighted selected content" - ); - assert.notOk( - this.get("subject") - .rowByValue("joffrey") - .exists(), - "generated content shouldn’t appear in content when removed" - ); - }); + const $lastSelectedName1 = this.get("subject") + .header() + .el() + .find(".selected-name") + .last(); + assert.equal( + $lastSelectedName1.attr("data-name"), + "robin", + "it removes the previous highlighted selected content" + ); + assert.notOk( + this.get("subject") + .rowByValue("joffrey") + .exists(), + "generated content shouldn’t appear in content when removed" + ); - this.get("subject") - .keyboard() - .selectAll(); + await this.get("subject").keyboard("selectAll"); - andThen(() => { - const $highlightedSelectedNames = this.get("subject") - .header() - .el() - .find(".selected-name.is-highlighted"); - assert.equal( - $highlightedSelectedNames.length, - 3, - "it highlights each selected name" - ); - }); + const $highlightedSelectedNames2 = this.get("subject") + .header() + .el() + .find(".selected-name.is-highlighted"); + assert.equal( + $highlightedSelectedNames2.length, + 3, + "it highlights each selected name" + ); - this.get("subject") - .keyboard() - .backspace(); + await this.get("subject").keyboard("backspace"); - andThen(() => { - const $selectedNames = this.get("subject") - .header() - .el() - .find(".selected-name"); - assert.equal($selectedNames.length, 0, "it removed all selected content"); - }); + const $selectedNames = this.get("subject") + .header() + .el() + .find(".selected-name"); + assert.equal($selectedNames.length, 0, "it removed all selected content"); - andThen(() => { - assert.ok(this.get("subject").isFocused()); - assert.ok(this.get("subject").isExpanded()); - }); + assert.ok(this.get("subject").isFocused()); + assert.ok(this.get("subject").isExpanded()); - this.get("subject") - .keyboard() - .escape(); + await this.get("subject").keyboard("escape"); - andThen(() => { - assert.ok(this.get("subject").isFocused()); - assert.notOk(this.get("subject").isExpanded()); - }); + assert.ok(this.get("subject").isFocused()); + assert.notOk(this.get("subject").isExpanded()); - this.get("subject") - .keyboard() - .escape(); + await this.get("subject").keyboard("escape"); - andThen(() => { - assert.notOk(this.get("subject").isFocused()); - assert.notOk(this.get("subject").isExpanded()); - }); + assert.notOk(this.get("subject").isFocused()); + assert.notOk(this.get("subject").isExpanded()); } }); @@ -227,16 +185,14 @@ componentTest("with limitMatches", { this.set("content", ["sam", "jeff", "neil"]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal( - this.get("subject") - .el() - .find(".select-kit-row").length, - 2 - ) + assert.equal( + this.get("subject") + .el() + .find(".select-kit-row").length, + 2 ); } }); @@ -248,26 +204,22 @@ componentTest("with minimum", { this.set("content", ["sam", "jeff", "neil"]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal( - this.get("subject").validationMessage(), - "Select at least 1 item." - ) + assert.equal( + this.get("subject").validationMessage(), + "Select at least 1 item." ); - this.get("subject").selectRowByValue("sam"); + await this.get("subject").selectRowByValue("sam"); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .label(), - "sam" - ); - }); + assert.equal( + this.get("subject") + .header() + .label(), + "sam" + ); } }); @@ -280,22 +232,18 @@ componentTest("with minimumLabel", { this.set("content", ["sam", "jeff", "neil"]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal(this.get("subject").validationMessage(), "min 1") + assert.equal(this.get("subject").validationMessage(), "min 1"); + + await this.get("subject").selectRowByValue("jeff"); + + assert.equal( + this.get("subject") + .header() + .label(), + "jeff" ); - - this.get("subject").selectRowByValue("jeff"); - - andThen(() => { - assert.equal( - this.get("subject") - .header() - .label(), - "jeff" - ); - }); } }); diff --git a/test/javascripts/components/single-select-test.js.es6 b/test/javascripts/components/single-select-test.js.es6 index 46caaa6616..da1e9fbe56 100644 --- a/test/javascripts/components/single-select-test.js.es6 +++ b/test/javascripts/components/single-select-test.js.es6 @@ -16,30 +16,24 @@ componentTest("updating the content refreshes the list", { this.set("content", [{ id: 1, name: "BEFORE" }]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .rowByValue(1) - .name(), - "BEFORE" - ); - }); + assert.equal( + this.get("subject") + .rowByValue(1) + .name(), + "BEFORE" + ); - andThen(() => { - this.set("content", [{ id: 1, name: "AFTER" }]); - }); + await this.set("content", [{ id: 1, name: "AFTER" }]); - andThen(() => { - assert.equal( - this.get("subject") - .rowByValue(1) - .name(), - "AFTER" - ); - }); + assert.equal( + this.get("subject") + .rowByValue(1) + .name(), + "AFTER" + ); } }); @@ -51,24 +45,20 @@ componentTest("accepts a value by reference", { this.set("content", [{ id: 1, name: "robin" }, { id: 2, name: "regis" }]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .selectedRow() - .name(), - "robin", - "it highlights the row corresponding to the value" - ); - }); + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "robin", + "it highlights the row corresponding to the value" + ); - this.get("subject").selectRowByValue(1); + await this.get("subject").selectRowByValue(1); - andThen(() => { - assert.equal(this.get("value"), 1, "it mutates the value"); - }); + assert.equal(this.get("value"), 1, "it mutates the value"); } }); @@ -89,69 +79,63 @@ componentTest("no default icon", { componentTest("default search icon", { template: "{{single-select filterable=true}}", - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.ok( - exists( - this.get("subject") - .filter() - .icon() - ), - "it has an icon" - ); - }); + assert.ok( + exists( + this.get("subject") + .filter() + .icon() + ), + "it has an icon" + ); } }); componentTest("with no search icon", { template: "{{single-select filterable=true filterIcon=null}}", - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.notOk( - exists( - this.get("subject") - .filter() - .icon() - ), - "it has no icon" - ); - }); + assert.notOk( + exists( + this.get("subject") + .filter() + .icon() + ), + "it has no icon" + ); } }); componentTest("custom search icon", { template: '{{single-select filterable=true filterIcon="shower"}}', - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.ok( - this.get("subject") - .filter() - .icon() - .hasClass("d-icon-shower"), - "it has a the correct icon" - ); - }); + assert.ok( + this.get("subject") + .filter() + .icon() + .hasClass("d-icon-shower"), + "it has a the correct icon" + ); } }); componentTest("is expandable", { template: "{{single-select}}", - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => assert.ok(this.get("subject").isExpanded())); + assert.ok(this.get("subject").isExpanded()); - this.get("subject").collapse(); + await this.get("subject").collapse(); - andThen(() => assert.notOk(this.get("subject").isExpanded())); + assert.notOk(this.get("subject").isExpanded()); } }); @@ -164,17 +148,15 @@ componentTest("accepts custom value/name keys", { this.set("content", [{ identifier: 1, item: "robin" }]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .selectedRow() - .name(), - "robin" - ); - }); + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "robin" + ); } }); @@ -185,14 +167,12 @@ componentTest("doesn’t render collection content before first expand", { this.set("content", [{ value: 1, name: "robin" }]); }, - test(assert) { + async test(assert) { assert.notOk(exists(find(".select-kit-collection"))); - this.get("subject").expand(); + await this.get("subject").expand(); - andThen(() => { - assert.ok(exists(find(".select-kit-collection"))); - }); + assert.ok(exists(find(".select-kit-collection"))); } }); @@ -203,29 +183,25 @@ componentTest("dynamic headerText", { this.set("content", [{ id: 1, name: "robin" }, { id: 2, name: "regis" }]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .name(), - "robin" - ); - }); + assert.equal( + this.get("subject") + .header() + .name(), + "robin" + ); - this.get("subject").selectRowByValue(2); + await this.get("subject").selectRowByValue(2); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .name(), - "regis", - "it changes header text" - ); - }); + assert.equal( + this.get("subject") + .header() + .name(), + "regis", + "it changes header text" + ); } }); @@ -239,19 +215,17 @@ componentTest("supports custom row template", { }); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .rowByValue(1) - .el() - .html() - .trim(), - "robin" - ); - }); + assert.equal( + this.get("subject") + .rowByValue(1) + .el() + .html() + .trim(), + "robin" + ); } }); @@ -266,31 +240,25 @@ componentTest("supports converting select value to integer", { ]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal( - this.get("subject") - .selectedRow() - .name(), - "régis" - ) + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "régis" ); - andThen(() => { - this.set("value", 1); - }); + await this.set("value", 1); - andThen(() => { - assert.equal( - this.get("subject") - .selectedRow() - .name(), - "robin", - "it works with dynamic content" - ); - }); + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "robin", + "it works with dynamic content" + ); } }); @@ -305,31 +273,25 @@ componentTest("supports converting string as boolean to boolean", { ]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal( - this.get("subject") - .selectedRow() - .name(), - "ASC" - ) + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "ASC" ); - andThen(() => { - this.set("value", false); - }); + await this.set("value", false); - andThen(() => { - assert.equal( - this.get("subject") - .selectedRow() - .name(), - "DESC", - "it works with dynamic content" - ); - }); + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "DESC", + "it works with dynamic content" + ); } }); @@ -340,92 +302,68 @@ componentTest("supports keyboard events", { this.set("content", [{ id: 1, name: "robin" }, { id: 2, name: "regis" }]); }, - test(assert) { - this.get("subject") - .expand() - .keyboard() - .down(); + async test(assert) { + await this.get("subject").expand(); + await this.get("subject").keyboard("down"); - andThen(() => { - assert.equal( - this.get("subject") - .highlightedRow() - .title(), - "regis", - "the next row is highlighted" - ); - }); + assert.equal( + this.get("subject") + .highlightedRow() + .title(), + "regis", + "the next row is highlighted" + ); - this.get("subject") - .keyboard() - .down(); + await this.get("subject").keyboard("down"); - andThen(() => { - assert.equal( - this.get("subject") - .highlightedRow() - .title(), - "robin", - "it returns to the first row" - ); - }); + assert.equal( + this.get("subject") + .highlightedRow() + .title(), + "robin", + "it returns to the first row" + ); - this.get("subject") - .keyboard() - .up(); + await this.get("subject").keyboard("up"); - andThen(() => { - assert.equal( - this.get("subject") - .highlightedRow() - .title(), - "regis", - "it highlights the last row" - ); - }); + assert.equal( + this.get("subject") + .highlightedRow() + .title(), + "regis", + "it highlights the last row" + ); - this.get("subject") - .keyboard() - .enter(); + await this.get("subject").keyboard("enter"); - andThen(() => { - assert.equal( - this.get("subject") - .selectedRow() - .title(), - "regis", - "it selects the row when pressing enter" - ); - assert.notOk( - this.get("subject").isExpanded(), - "it collapses the select box when selecting a row" - ); - }); + assert.equal( + this.get("subject") + .selectedRow() + .title(), + "regis", + "it selects the row when pressing enter" + ); + assert.notOk( + this.get("subject").isExpanded(), + "it collapses the select box when selecting a row" + ); - this.get("subject") - .expand() - .keyboard() - .escape(); + await this.get("subject").expand(); + await this.get("subject").keyboard("escape"); - andThen(() => { - assert.notOk( - this.get("subject").isExpanded(), - "it collapses the select box" - ); - }); + assert.notOk( + this.get("subject").isExpanded(), + "it collapses the select box" + ); - this.get("subject") - .expand() - .fillInFilter("regis") - .keyboard() - .tab(); + await this.get("subject").expand(); + await this.get("subject").fillInFilter("regis"); + await this.get("subject").keyboard("tab"); - andThen(() => { - assert.notOk( - this.get("subject").isExpanded(), - "it collapses the select box when selecting a row" - ); - }); + assert.notOk( + this.get("subject").isExpanded(), + "it collapses the select box when selecting a row" + ); } }); @@ -442,13 +380,11 @@ componentTest("with allowInitialValueMutation", { }, test(assert) { - andThen(() => { - assert.equal( - this.get("value"), - "1", - "it mutates the value on initial rendering" - ); - }); + assert.equal( + this.get("value"), + "1", + "it mutates the value on initial rendering" + ); } }); @@ -464,20 +400,18 @@ componentTest("support appending content through plugin api", { this.set("content", [{ id: "1", name: "robin" }]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal(this.get("subject").rows().length, 2); - assert.equal( - this.get("subject") - .rowByIndex(1) - .name(), - "regis" - ); - }); + assert.equal(this.get("subject").rows().length, 2); + assert.equal( + this.get("subject") + .rowByIndex(1) + .name(), + "regis" + ); - andThen(() => clearCallbacks()); + clearCallbacks(); } }); @@ -500,20 +434,18 @@ componentTest("support modifying content through plugin api", { ]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal(this.get("subject").rows().length, 3); - assert.equal( - this.get("subject") - .rowByIndex(1) - .name(), - "sam" - ); - }); + assert.equal(this.get("subject").rows().length, 3); + assert.equal( + this.get("subject") + .rowByIndex(1) + .name(), + "sam" + ); - andThen(() => clearCallbacks()); + clearCallbacks(); } }); @@ -530,20 +462,18 @@ componentTest("support prepending content through plugin api", { this.set("content", [{ id: "1", name: "robin" }]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal(this.get("subject").rows().length, 2); - assert.equal( - this.get("subject") - .rowByIndex(0) - .name(), - "regis" - ); - }); + assert.equal(this.get("subject").rows().length, 2); + assert.equal( + this.get("subject") + .rowByIndex(0) + .name(), + "regis" + ); - andThen(() => clearCallbacks()); + clearCallbacks(); } }); @@ -561,16 +491,13 @@ componentTest("support modifying on select behavior through plugin api", { this.set("content", [{ id: "1", name: "robin" }]); }, - test(assert) { - this.get("subject") - .expand() - .selectRowByValue(1); + async test(assert) { + await this.get("subject").expand(); + await this.get("subject").selectRowByValue(1); - andThen(() => { - assert.equal(find(".on-select-test").html(), "1"); - }); + assert.equal(find(".on-select-test").html(), "1"); - andThen(() => clearCallbacks()); + clearCallbacks(); } }); @@ -582,30 +509,24 @@ componentTest("with nameChanges", { this.set("content", [this.get("robin")]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .name(), - "robin" - ); - }); + assert.equal( + this.get("subject") + .header() + .name(), + "robin" + ); - andThen(() => { - this.set("robin.name", "robin2"); - }); + await this.set("robin.name", "robin2"); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .name(), - "robin2" - ); - }); + assert.equal( + this.get("subject") + .header() + .name(), + "robin2" + ); } }); @@ -616,23 +537,21 @@ componentTest("with null value", { this.set("content", [{ name: "robin" }]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .name(), - "robin" - ); - assert.equal( - this.get("subject") - .header() - .value(), - undefined - ); - }); + assert.equal( + this.get("subject") + .header() + .name(), + "robin" + ); + assert.equal( + this.get("subject") + .header() + .value(), + undefined + ); } }); @@ -643,10 +562,10 @@ componentTest("with collection header", { this.set("collectionHeader", "

Hello

"); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => assert.ok(exists(".collection-header h2"))); + assert.ok(exists(".collection-header h2")); } }); @@ -658,13 +577,11 @@ componentTest("with title", { }, test(assert) { - andThen(() => - assert.equal( - this.get("subject") - .header() - .title(), - "My title" - ) + assert.equal( + this.get("subject") + .header() + .title(), + "My title" ); } }); @@ -686,16 +603,14 @@ componentTest("support modifying header computed content through plugin api", { }, test(assert) { - andThen(() => { - assert.equal( - this.get("subject") - .header() - .title(), - "Not so evil" - ); - }); + assert.equal( + this.get("subject") + .header() + .title(), + "Not so evil" + ); - andThen(() => clearCallbacks()); + clearCallbacks(); } }); @@ -706,16 +621,14 @@ componentTest("with limitMatches", { this.set("content", ["sam", "jeff", "neil"]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal( - this.get("subject") - .el() - .find(".select-kit-row").length, - 2 - ) + assert.equal( + this.get("subject") + .el() + .find(".select-kit-row").length, + 2 ); } }); @@ -728,26 +641,22 @@ componentTest("with minimum", { this.set("content", ["sam", "jeff", "neil"]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal( - this.get("subject").validationMessage(), - "Select at least 1 item." - ) + assert.equal( + this.get("subject").validationMessage(), + "Select at least 1 item." ); - this.get("subject").selectRowByValue("sam"); + await this.get("subject").selectRowByValue("sam"); - andThen(() => { - assert.equal( - this.get("subject") - .header() - .label(), - "sam" - ); - }); + assert.equal( + this.get("subject") + .header() + .label(), + "sam" + ); } }); @@ -760,23 +669,19 @@ componentTest("with minimumLabel", { this.set("content", ["sam", "jeff", "neil"]); }, - test(assert) { - this.get("subject").expand(); + async test(assert) { + await this.get("subject").expand(); - andThen(() => - assert.equal(this.get("subject").validationMessage(), "min 1") + assert.equal(this.get("subject").validationMessage(), "min 1"); + + await this.get("subject").selectRowByValue("jeff"); + + assert.equal( + this.get("subject") + .header() + .label(), + "jeff" ); - - this.get("subject").selectRowByValue("jeff"); - - andThen(() => { - assert.equal( - this.get("subject") - .header() - .label(), - "jeff" - ); - }); } }); @@ -787,19 +692,17 @@ componentTest("with accents in filter", { this.set("content", ["sam", "jeff", "neil"]); }, - test(assert) { - this.get("subject").expand(); - this.get("subject").fillInFilter("jéff"); + async test(assert) { + await this.get("subject").expand(); + await this.get("subject").fillInFilter("jéff"); - andThen(() => { - assert.equal(this.get("subject").rows().length, 1); - assert.equal( - this.get("subject") - .rowByIndex(0) - .name(), - "jeff" - ); - }); + assert.equal(this.get("subject").rows().length, 1); + assert.equal( + this.get("subject") + .rowByIndex(0) + .name(), + "jeff" + ); } }); @@ -810,18 +713,16 @@ componentTest("with accents in content", { this.set("content", ["sam", "jéff", "neil"]); }, - test(assert) { - this.get("subject").expand(); - this.get("subject").fillInFilter("jeff"); + async test(assert) { + await this.get("subject").expand(); + await this.get("subject").fillInFilter("jeff"); - andThen(() => { - assert.equal(this.get("subject").rows().length, 1); - assert.equal( - this.get("subject") - .rowByIndex(0) - .name(), - "jéff" - ); - }); + assert.equal(this.get("subject").rows().length, 1); + assert.equal( + this.get("subject") + .rowByIndex(0) + .name(), + "jéff" + ); } }); diff --git a/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 b/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 index 6ba9d0f1b0..1a62d26779 100644 --- a/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 +++ b/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 @@ -22,7 +22,7 @@ componentTest("default", { }, async test(assert) { - await this.get("subject").expandAwait(); + await this.get("subject").expand(); assert.equal( this.get("subject") @@ -55,7 +55,7 @@ componentTest("default", { "it doesn’t preselect first row" ); - await this.get("subject").selectRowByValueAwait("share"); + await this.get("subject").selectRowByValue("share"); assert.equal(this.get("value"), null, "it resets the value"); } diff --git a/test/javascripts/components/topic-notifications-options-test.js.es6 b/test/javascripts/components/topic-notifications-options-test.js.es6 index e8d7b8bb98..05ae0d9dbb 100644 --- a/test/javascripts/components/topic-notifications-options-test.js.es6 +++ b/test/javascripts/components/topic-notifications-options-test.js.es6 @@ -33,7 +33,7 @@ componentTest("regular topic notification level descriptions", { "{{topic-notifications-options value=topic.details.notification_level topic=topic}}", async test(assert) { - await selectKit().expandAwait(); + await selectKit().expand(); await this.set("topic", buildTopic("regular")); const uiTexts = extractDescs(selectKit().rows()); @@ -59,7 +59,7 @@ componentTest("PM topic notification level descriptions", { "{{topic-notifications-options value=topic.details.notification_level topic=topic}}", async test(assert) { - await selectKit().expandAwait(); + await selectKit().expand(); await this.set("topic", buildTopic("private_message")); const uiTexts = extractDescs(selectKit().rows()); diff --git a/test/javascripts/helpers/d-editor-helper.js b/test/javascripts/helpers/d-editor-helper.js new file mode 100644 index 0000000000..4245437645 --- /dev/null +++ b/test/javascripts/helpers/d-editor-helper.js @@ -0,0 +1,15 @@ +Ember.Test.registerAsyncHelper("formatTextWithSelection", function( + app, + text, + [start, len] +) { + return [ + '"', + text.substr(0, start), + "<", + text.substr(start, len), + ">", + text.substr(start + len), + '"' + ].join(""); +}); diff --git a/test/javascripts/helpers/select-kit-helper.js b/test/javascripts/helpers/select-kit-helper.js index 9c80847a06..e170d8f510 100644 --- a/test/javascripts/helpers/select-kit-helper.js +++ b/test/javascripts/helpers/select-kit-helper.js @@ -68,6 +68,47 @@ Ember.Test.registerAsyncHelper("selectKitSelectRowByIndex", function( click(find(selector + " .select-kit-row").eq(index)); }); +Ember.Test.registerAsyncHelper("keyboardHelper", function( + app, + value, + target, + selector +) { + function createEvent(element, keyCode, options) { + element = element || ".filter-input"; + selector = find(selector).find(element); + options = options || {}; + + var type = options.type || "keydown"; + var event = jQuery.Event(type); + event.keyCode = keyCode; + if (options && options.metaKey) { + event.metaKey = true; + } + + andThen(() => { + find(selector).trigger(event); + }); + } + + switch (value) { + case "enter": + return createEvent(target, 13); + case "backspace": + return createEvent(target, 8); + case "selectAll": + return createEvent(target, 65, { metaKey: true }); + case "escape": + return createEvent(target, 27); + case "down": + return createEvent(target, 40); + case "up": + return createEvent(target, 38); + case "tab": + return createEvent(target, 9); + } +}); + // eslint-disable-next-line no-unused-vars function selectKit(selector) { selector = selector || ".select-kit"; @@ -132,65 +173,13 @@ function selectKit(selector) { }; } - function keyboardHelper(eventSelector) { - function createEvent(target, keyCode, options) { - target = target || ".filter-input"; - eventSelector = find(eventSelector).find(target); - options = options || {}; - - andThen(function() { - var type = options.type || "keydown"; - var event = jQuery.Event(type); - event.keyCode = keyCode; - if (options && options.metaKey) { - event.metaKey = true; - } - find(eventSelector).trigger(event); - }); - } - - return { - down: function(target) { - createEvent(target, 40); - }, - up: function(target) { - createEvent(target, 38); - }, - escape: function(target) { - createEvent(target, 27); - }, - enter: function(target) { - createEvent(target, 13); - }, - tab: function(target) { - createEvent(target, 9); - }, - backspace: function(target) { - createEvent(target, 8); - }, - selectAll: function(target) { - createEvent(target, 65, { metaKey: true }); - } - }; - } - return { - expandAwait: function() { + expand: function() { return expandSelectKit(selector); }, - expand: function() { - expandSelectKit(selector); - return selectKit(selector); - }, - - collapseAwait: function() { - return collapseSelectKit(selector); - }, - collapse: function() { - collapseSelectKit(selector); - return selectKit(selector); + return collapseSelectKit(selector); }, selectRowByIndex: function(index) { @@ -198,13 +187,8 @@ function selectKit(selector) { return selectKit(selector); }, - selectRowByValueAwait: function(value) { - return selectKitSelectRowByValue(value, selector); - }, - selectRowByValue: function(value) { - selectKitSelectRowByValue(value, selector); - return selectKit(selector); + return selectKitSelectRowByValue(value, selector); }, selectRowByName: function(name) { @@ -213,25 +197,15 @@ function selectKit(selector) { }, selectNoneRow: function() { - selectKitSelectNoneRow(selector); - return selectKit(selector); - }, - - selectNoneRowAwait: function() { return selectKitSelectNoneRow(selector); }, fillInFilter: function(filter) { - selectKitFillInFilter(filter, selector); - return selectKit(selector); - }, - - fillInFilterAwait: function(filter) { return selectKitFillInFilter(filter, selector); }, - keyboard: function() { - return keyboardHelper(selector); + keyboard: function(value, target) { + return keyboardHelper(value, target, selector); }, isExpanded: function() { diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index e5832987b4..b728169e11 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -33,6 +33,7 @@ //= require helpers/assertions //= require helpers/select-kit-helper +//= require helpers/d-editor-helper //= require helpers/qunit-helpers //= require_tree ./fixtures From 8d1acbd4c2af871b48509ece0d5a69124b5c429f Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 30 Jul 2018 11:33:48 +0100 Subject: [PATCH 043/168] DEV: Include specific authenticator name in warning message --- lib/plugin/instance.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 42f86f87f4..3eb543c1fc 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -506,9 +506,9 @@ JS provider.authenticator.enabled? rescue NotImplementedError provider.authenticator.define_singleton_method(:enabled?) do - Rails.logger.warn("Auth::Authenticator subclasses should define an `enabled?` function. Patching for now.") + Rails.logger.warn("#{provider.authenticator.class.name} should define an `enabled?` function. Patching for now.") return SiteSetting.send(provider.enabled_setting) if provider.enabled_setting - Rails.logger.warn("Plugin::AuthProvider has not defined an enabled_setting. Defaulting to true.") + Rails.logger.warn("#{provider.authenticator.class.name} has not defined an enabled_setting. Defaulting to true.") true end end From 50df2d7241574f7039f2e8751eb04141c1355957 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Mon, 30 Jul 2018 16:06:36 +0530 Subject: [PATCH 044/168] FIX: Should not include regular categories in top_category_ids array --- app/serializers/current_user_serializer.rb | 3 ++- spec/serializers/current_user_serializer_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 0b801b0942..c55d3ece8a 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -158,8 +158,9 @@ class CurrentUserSerializer < BasicUserSerializer end def top_category_ids + omitted_notification_levels = [CategoryUser.notification_levels[:muted], CategoryUser.notification_levels[:regular]] CategoryUser.where(user_id: object.id) - .where.not(notification_level: CategoryUser.notification_levels[:muted]) + .where.not(notification_level: omitted_notification_levels) .order(" CASE WHEN notification_level = 3 THEN 1 diff --git a/spec/serializers/current_user_serializer_spec.rb b/spec/serializers/current_user_serializer_spec.rb index 46ca334f71..45982c374a 100644 --- a/spec/serializers/current_user_serializer_spec.rb +++ b/spec/serializers/current_user_serializer_spec.rb @@ -37,6 +37,7 @@ RSpec.describe CurrentUserSerializer do let(:user) { Fabricate(:user) } let(:category1) { Fabricate(:category) } let(:category2) { Fabricate(:category) } + let(:category3) { Fabricate(:category) } let :serializer do CurrentUserSerializer.new(user, scope: Guardian.new, root: false) end @@ -55,6 +56,11 @@ RSpec.describe CurrentUserSerializer do CategoryUser.create!(user_id: user.id, category_id: category2.id, notification_level: CategoryUser.notification_levels[:watching]) + + CategoryUser.create!(user_id: user.id, + category_id: category3.id, + notification_level: CategoryUser.notification_levels[:regular]) + payload = serializer.as_json expect(payload[:top_category_ids]).to eq([category2.id, category1.id]) end From c7c84606d931ebfb1fe44d5d7c40a19bafe130f6 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 30 Jul 2018 12:43:10 +0200 Subject: [PATCH 045/168] fix prettier offense --- test/javascripts/acceptance/preferences-test.js.es6 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index 51a7eb0418..f278084e29 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -58,9 +58,7 @@ QUnit.test("update some fields", async assert => { await savePreferences(); click(".preferences-nav .nav-notifications a"); - await selectKit( - ".control-group.notifications .combo-box.duration" - ).expand(); + await selectKit(".control-group.notifications .combo-box.duration").expand(); await selectKit( ".control-group.notifications .combo-box.duration" ).selectRowByValue(1440); From dfcb2a0d42d1a34733d46037c4dc86611c79b48b Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 30 Jul 2018 16:22:51 +0530 Subject: [PATCH 046/168] FEATURE: include published_time in metadata --- app/helpers/application_helper.rb | 4 ++++ app/views/topics/plain.html.erb | 2 +- app/views/topics/show.html.erb | 2 +- lib/topic_view.rb | 5 +++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 098d540a0d..90d148be7f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -237,6 +237,10 @@ module ApplicationHelper result << tag(:meta, name: 'twitter:data2', value: "#{opts[:like_count]} ❤") end + if opts[:published_time] + result << tag(:meta, property: 'article:published_time', content: opts[:published_time]) + end + if opts[:ignore_canonical] result << tag(:meta, property: 'og:ignore_canonical', content: true) end diff --git a/app/views/topics/plain.html.erb b/app/views/topics/plain.html.erb index 7e7e31bd02..52d6d5cbdb 100644 --- a/app/views/topics/plain.html.erb +++ b/app/views/topics/plain.html.erb @@ -3,7 +3,7 @@ <%= @topic_view.topic.title %> - <%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary(strip_images: true), image: @topic_view.image_url, read_time: @topic_view.read_time, like_count: @topic_view.like_count) %> + <%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary(strip_images: true), image: @topic_view.image_url, read_time: @topic_view.read_time, like_count: @topic_view.like_count, published_time: @topic_view.published_time) %> <% if @topic_view.prev_page %> <% end %> diff --git a/app/views/topics/show.html.erb b/app/views/topics/show.html.erb index c67fd23ecf..21788d3b77 100644 --- a/app/views/topics/show.html.erb +++ b/app/views/topics/show.html.erb @@ -88,7 +88,7 @@ <% content_for :head do %> <%= auto_discovery_link_tag(@topic_view, {action: :feed, slug: @topic_view.topic.slug, topic_id: @topic_view.topic.id}, title: t('rss_posts_in_topic', topic: @topic_view.title), type: 'application/rss+xml') %> - <%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary(strip_images: true), image: @topic_view.image_url, read_time: @topic_view.read_time, like_count: @topic_view.like_count, ignore_canonical: true) %> + <%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary(strip_images: true), image: @topic_view.image_url, read_time: @topic_view.read_time, like_count: @topic_view.like_count, ignore_canonical: true, published_time: @topic_view.published_time) %> <% if @topic_view.prev_page || @topic_view.next_page %> <% if @topic_view.prev_page %> diff --git a/lib/topic_view.rb b/lib/topic_view.rb index f794e9477c..625c9ef433 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -193,6 +193,11 @@ class TopicView @topic.like_count end + def published_time + return nil if desired_post.blank? + desired_post.created_at.strftime('%FT%T%:z') + end + def image_url if @post_number > 1 && @desired_post.present? if @desired_post.image_url.present? From d494feaa32698a56d6073c7e06b31269bc3f5300 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 30 Jul 2018 09:31:27 -0400 Subject: [PATCH 047/168] FIX: should not be needed as we have itemprop='url' --- app/views/list/list.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/list/list.erb b/app/views/list/list.erb index ca71796aea..cb2dc7d37e 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -44,7 +44,7 @@
- + <%= t.title %> <%= page_links(t) %> From 6566b2f11a30da71128264413a357f86b038d691 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 30 Jul 2018 14:38:53 +0100 Subject: [PATCH 048/168] FEATURE: Allow revoke and connect for Instagram logins --- .../discourse/models/login-method.js.es6 | 11 +++++-- lib/auth/instagram_authenticator.rb | 31 ++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 index 5105179104..9b4d83d423 100644 --- a/app/assets/javascripts/discourse/models/login-method.js.es6 +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -128,9 +128,14 @@ export function findAll(siteSettings, capabilities, isMobileDevice) { } if ( - ["facebook", "google_oauth2", "twitter", "yahoo", "github"].includes( - name - ) + [ + "facebook", + "google_oauth2", + "twitter", + "yahoo", + "github", + "instagram" + ].includes(name) ) { params.canConnect = true; } diff --git a/lib/auth/instagram_authenticator.rb b/lib/auth/instagram_authenticator.rb index ca4f41ebc8..515616fbbc 100644 --- a/lib/auth/instagram_authenticator.rb +++ b/lib/auth/instagram_authenticator.rb @@ -13,9 +13,23 @@ class Auth::InstagramAuthenticator < Auth::Authenticator info&.screen_name || "" end - # TODO twitter provides all sorts of extra info, like website/bio etc. - # it may be worth considering pulling some of it in. - def after_authenticate(auth_token) + def can_revoke? + true + end + + def revoke(user, skip_remote: false) + info = InstagramUserInfo.find_by(user_id: user.id) + raise Discourse::NotFound if info.nil? + # Instagram does not have any way for us to revoke tokens on their end + info.destroy! + true + end + + def can_connect_existing_user? + true + end + + def after_authenticate(auth_token, existing_account: nil) result = Auth::Result.new @@ -32,7 +46,16 @@ class Auth::InstagramAuthenticator < Auth::Authenticator user_info = InstagramUserInfo.find_by(instagram_user_id: instagram_user_id) - result.user = user_info.try(:user) + if existing_account && (user_info.nil? || existing_account.id != user_info.user_id) + user_info.destroy! if user_info + user_info = InstagramUserInfo.create!( + user_id: existing_account.id, + screen_name: screen_name, + instagram_user_id: instagram_user_id + ) + end + + result.user = user_info&.user result end From c54b5824d474f0c7db948670f87a7a6f29f6fad6 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Mon, 30 Jul 2018 20:20:10 +0530 Subject: [PATCH 049/168] REFACTOR: Prioritize unread categories in hamburger menu --- .../discourse/widgets/hamburger-menu.js.es6 | 31 +++++++++++++++---- .../widgets/hamburger-menu-test.js.es6 | 25 +++++++++++---- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 3838d675e6..e8e2fed85c 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -3,6 +3,7 @@ import { h } from "virtual-dom"; import DiscourseURL from "discourse/lib/url"; import { ajax } from "discourse/lib/ajax"; import { userPath } from "discourse/lib/url"; +import { NotificationLevels } from "discourse/lib/notification-levels"; const flatten = array => [].concat.apply([], array); @@ -178,15 +179,33 @@ export default createWidget("hamburger-menu", { listCategories() { const maxCategoriesToDisplay = this.siteSettings .hamburger_menu_categories_count; - const categoriesList = this.site.get("categoriesByCount"); - let categories = categoriesList.slice(); + const categoriesByCount = this.site.get("categoriesByCount"); + let categories = categoriesByCount.slice(); if (this.currentUser) { - let categoryIds = this.currentUser.get("top_category_ids") || []; + let unreadCategoryIds = []; + let topCategoryIds = this.currentUser.get("top_category_ids") || []; let i = 0; - const mutedCategoryIds = this.currentUser.get("muted_category_ids") || []; - categories = categories.filter(c => !mutedCategoryIds.includes(c.id)); - categoryIds.forEach(id => { + + categoriesByCount + .sort((a, b) => { + return ( + b.get("newTopics") + + b.get("unreadTopics") - + (a.get("newTopics") + a.get("unreadTopics")) + ); + }) + .forEach(c => { + if (c.get("newTopics") > 0 || c.get("unreadTopics") > 0) { + unreadCategoryIds.push(c.id); + } + }); + + categories = categories.filter( + c => c.notification_level !== NotificationLevels.MUTED + ); + + [...unreadCategoryIds, ...topCategoryIds].uniq().forEach(id => { const category = categories.find(c => c.id === id); if (category) { categories = categories.filter(c => c.id !== id); diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 index ddb39f219d..c50323bf18 100644 --- a/test/javascripts/widgets/hamburger-menu-test.js.es6 +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -1,9 +1,11 @@ import { moduleForWidget, widgetTest } from "helpers/widget-test"; +import { NotificationLevels } from "discourse/lib/notification-levels"; moduleForWidget("hamburger-menu"); const topCategoryIds = [2, 3, 1]; let mutedCategoryIds = []; +let unreadCategoryIds = []; let categoriesByCount = []; widgetTest("prioritize faq", { @@ -155,16 +157,24 @@ widgetTest("top categories", { beforeEach() { this.siteSettings.hamburger_menu_categories_count = 8; maxCategoriesToDisplay = this.siteSettings.hamburger_menu_categories_count; - categoriesByCount = this.site.get("categoriesByCount"); + categoriesByCount = this.site.get("categoriesByCount").slice(); categoriesByCount.every(c => { if (!topCategoryIds.includes(c.id)) { - mutedCategoryIds.push(c.id); - return false; + if (mutedCategoryIds.length === 0) { + mutedCategoryIds.push(c.id); + c.set("notification_level", NotificationLevels.MUTED); + } else if (unreadCategoryIds.length === 0) { + unreadCategoryIds.push(c.id); + c.set("unreadTopics", 5); + } else { + unreadCategoryIds.splice(0, 0, c.id); + c.set("newTopics", 10); + return false; + } } return true; }); this.currentUser.set("top_category_ids", topCategoryIds); - this.currentUser.set("muted_category_ids", mutedCategoryIds); }, test(assert) { @@ -173,8 +183,11 @@ widgetTest("top categories", { categoriesByCount = categoriesByCount.filter( c => !mutedCategoryIds.includes(c.id) ); - let ids = topCategoryIds - .concat(categoriesByCount.map(c => c.id)) + let ids = [ + ...unreadCategoryIds, + ...topCategoryIds, + ...categoriesByCount.map(c => c.id) + ] .uniq() .slice(0, maxCategoriesToDisplay); From 17b851cf084d0b7b61c9a4e5d8b25d3c23291a66 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 30 Jul 2018 20:26:40 +0530 Subject: [PATCH 050/168] FEATURE: show last updated date for wiki topics --- lib/topic_view.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 625c9ef433..8020ba72de 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -195,7 +195,11 @@ class TopicView def published_time return nil if desired_post.blank? - desired_post.created_at.strftime('%FT%T%:z') + if desired_post.wiki && desired_post.post_number == 1 && desired_post.revisions.size > 0 + desired_post.revisions.last.updated_at.strftime('%FT%T%:z') + else + desired_post.created_at.strftime('%FT%T%:z') + end end def image_url From 1f899bec2103ade0dd40b65f986ae0ffc81cadf8 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 30 Jul 2018 11:09:08 -0400 Subject: [PATCH 051/168] DEV: prettier 1.14.0 --- .../javascripts/admin/models/admin-user.js.es6 | 9 ++++++--- .../discourse/components/backup-codes.js.es6 | 3 ++- .../discourse/components/d-navigation.js.es6 | 3 ++- .../discourse/components/group-post.js.es6 | 3 ++- .../discourse/components/topic-entrance.js.es6 | 6 ++++-- .../discourse/controllers/composer.js.es6 | 3 ++- .../javascripts/discourse/controllers/static.js.es6 | 3 ++- .../discourse/controllers/upload-selector.js.es6 | 6 ++++-- .../javascripts/discourse/models/composer.js.es6 | 6 ++++-- .../javascripts/discourse/models/nav-item.js.es6 | 3 ++- app/assets/javascripts/discourse/models/user.js.es6 | 12 ++++++++---- .../javascripts/wizard/components/staff-count.js.es6 | 3 ++- .../wizard/components/wizard-step-form.js.es6 | 3 ++- .../javascripts/wizard/components/wizard-step.js.es6 | 6 ++++-- .../javascripts/wizard/mixins/valid-state.js.es6 | 9 ++++++--- app/assets/javascripts/wizard/models/step.js.es6 | 3 ++- app/assets/javascripts/wizard/models/wizard.js.es6 | 3 ++- package.json | 2 +- yarn.lock | 6 +++--- 19 files changed, 60 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 39a2b7ee1d..ec230ffff4 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -530,11 +530,14 @@ const AdminUser = Discourse.User.extend({ } }, - @computed("suspended_by") suspendedBy: wrapAdmin, + @computed("suspended_by") + suspendedBy: wrapAdmin, - @computed("silenced_by") silencedBy: wrapAdmin, + @computed("silenced_by") + silencedBy: wrapAdmin, - @computed("approved_by") approvedBy: wrapAdmin + @computed("approved_by") + approvedBy: wrapAdmin }); AdminUser.reopenClass({ diff --git a/app/assets/javascripts/discourse/components/backup-codes.js.es6 b/app/assets/javascripts/discourse/components/backup-codes.js.es6 index 11457eb736..3df4b064b9 100644 --- a/app/assets/javascripts/discourse/components/backup-codes.js.es6 +++ b/app/assets/javascripts/discourse/components/backup-codes.js.es6 @@ -31,7 +31,8 @@ export default Ember.Component.extend({ } }, - @computed("formattedBackupCodes") base64BackupCode: b64EncodeUnicode, + @computed("formattedBackupCodes") + base64BackupCode: b64EncodeUnicode, @computed("backupCodes") formattedBackupCodes(backupCodes) { diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 index 0ba2a85201..dc1eee1c3c 100644 --- a/app/assets/javascripts/discourse/components/d-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -18,7 +18,8 @@ export default Ember.Component.extend({ return hasDraft ? "topic.open_draft" : "topic.create"; }, - @computed("category.can_edit") showCategoryEdit: canEdit => canEdit, + @computed("category.can_edit") + showCategoryEdit: canEdit => canEdit, @computed("filterMode", "category", "noSubcategories") navItems(filterMode, category, noSubcategories) { diff --git a/app/assets/javascripts/discourse/components/group-post.js.es6 b/app/assets/javascripts/discourse/components/group-post.js.es6 index 5a28daf92d..f94f36dc8d 100644 --- a/app/assets/javascripts/discourse/components/group-post.js.es6 +++ b/app/assets/javascripts/discourse/components/group-post.js.es6 @@ -1,5 +1,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("post.url") postUrl: Discourse.getURL + @computed("post.url") + postUrl: Discourse.getURL }); diff --git a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 index 997171aa97..216f024fc6 100644 --- a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 @@ -32,9 +32,11 @@ export default Ember.Component.extend(CleansUp, { topic: null, visible: null, - @computed("topic.created_at") createdDate: createdAt => new Date(createdAt), + @computed("topic.created_at") + createdDate: createdAt => new Date(createdAt), - @computed("topic.bumped_at") bumpedDate: bumpedAt => new Date(bumpedAt), + @computed("topic.bumped_at") + bumpedDate: bumpedAt => new Date(bumpedAt), @computed("createdDate", "bumpedDate") showTime(createdDate, bumpedDate) { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 870f60b854..f3f2675490 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -287,7 +287,8 @@ export default Ember.Controller.extend({ return authorizesOneOrMoreExtensions(); }, - @computed() uploadIcon: () => uploadIcon(), + @computed() + uploadIcon: () => uploadIcon(), actions: { cancelUpload() { diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index 6d3cbd2df1..8238d5fad9 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -7,7 +7,8 @@ export default Ember.Controller.extend({ showLoginButton: Em.computed.equal("model.path", "login"), - @computed("model.path") bodyClass: path => `static-${path}`, + @computed("model.path") + bodyClass: path => `static-${path}`, @computed("model.path") showSignupButton() { diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index 918128d0c2..b22674657b 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -25,9 +25,11 @@ export default Ember.Controller.extend(ModalFunctionality, { remote: Ember.computed.equal("selection", "remote"), selection: "local", - @computed() uploadIcon: () => uploadIcon(), + @computed() + uploadIcon: () => uploadIcon(), - @computed() title: () => uploadTranslate("title"), + @computed() + title: () => uploadTranslate("title"), @computed("selection") tip(selection) { diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 753c9bf741..801b44c24c 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -75,7 +75,8 @@ const Composer = RestModel.extend({ return this.site.get("archetypes"); }.property(), - @computed("action") sharedDraft: action => action === CREATE_SHARED_DRAFT, + @computed("action") + sharedDraft: action => action === CREATE_SHARED_DRAFT, @computed categoryId: { @@ -133,7 +134,8 @@ const Composer = RestModel.extend({ topicFirstPost: Em.computed.or("creatingTopic", "editingFirstPost"), - @computed("action") editingPost: isEdit, + @computed("action") + editingPost: isEdit, replyingToTopic: Em.computed.equal("action", REPLY), diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index ad4c0b1695..6a9a782aea 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -105,7 +105,8 @@ const NavItem = Discourse.Model.extend({ }); const ExtraNavItem = NavItem.extend({ - @computed("href") href: href => href, + @computed("href") + href: href => href, customFilter: null }); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 74f103e551..94f972ca27 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -198,13 +198,17 @@ const User = RestModel.extend({ return suspendedTill && moment(suspendedTill).isAfter(); }, - @computed("suspended_till") suspendedForever: isForever, + @computed("suspended_till") + suspendedForever: isForever, - @computed("silenced_till") silencedForever: isForever, + @computed("silenced_till") + silencedForever: isForever, - @computed("suspended_till") suspendedTillDate: longDate, + @computed("suspended_till") + suspendedTillDate: longDate, - @computed("silenced_till") silencedTillDate: longDate, + @computed("silenced_till") + silencedTillDate: longDate, changeUsername(new_username) { return ajax( diff --git a/app/assets/javascripts/wizard/components/staff-count.js.es6 b/app/assets/javascripts/wizard/components/staff-count.js.es6 index 17df6043d1..7677d999de 100644 --- a/app/assets/javascripts/wizard/components/staff-count.js.es6 +++ b/app/assets/javascripts/wizard/components/staff-count.js.es6 @@ -1,5 +1,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("field.value") showStaffCount: staffCount => staffCount > 1 + @computed("field.value") + showStaffCount: staffCount => staffCount > 1 }); diff --git a/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 b/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 index ff2919bebb..a60d5b39cb 100644 --- a/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 @@ -3,5 +3,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ classNameBindings: [":wizard-step-form", "customStepClass"], - @computed("step.id") customStepClass: stepId => `wizard-step-${stepId}` + @computed("step.id") + customStepClass: stepId => `wizard-step-${stepId}` }); diff --git a/app/assets/javascripts/wizard/components/wizard-step.js.es6 b/app/assets/javascripts/wizard/components/wizard-step.js.es6 index d5fb39af76..4732548e8d 100644 --- a/app/assets/javascripts/wizard/components/wizard-step.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step.js.es6 @@ -31,7 +31,8 @@ export default Ember.Component.extend({ this.autoFocus(); }, - @computed("step.index") showQuitButton: index => index === 0, + @computed("step.index") + showQuitButton: index => index === 0, @computed("step.displayIndex", "wizard.totalSteps") showNextButton: (current, total) => current < total, @@ -39,7 +40,8 @@ export default Ember.Component.extend({ @computed("step.displayIndex", "wizard.totalSteps") showDoneButton: (current, total) => current === total, - @computed("step.index") showBackButton: index => index > 0, + @computed("step.index") + showBackButton: index => index > 0, @computed("step.banner") bannerImage(src) { diff --git a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 index cd3faa5e69..ee00842eac 100644 --- a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 +++ b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 @@ -15,11 +15,14 @@ export default { this.set("_validState", States.UNCHECKED); }, - @computed("_validState") valid: state => state === States.VALID, + @computed("_validState") + valid: state => state === States.VALID, - @computed("_validState") invalid: state => state === States.INVALID, + @computed("_validState") + invalid: state => state === States.INVALID, - @computed("_validState") unchecked: state => state === States.UNCHECKED, + @computed("_validState") + unchecked: state => state === States.UNCHECKED, setValid(valid, description) { this.set("_validState", valid ? States.VALID : States.INVALID); diff --git a/app/assets/javascripts/wizard/models/step.js.es6 b/app/assets/javascripts/wizard/models/step.js.es6 index fc1d0ac1f5..d465b553b4 100644 --- a/app/assets/javascripts/wizard/models/step.js.es6 +++ b/app/assets/javascripts/wizard/models/step.js.es6 @@ -5,7 +5,8 @@ import { ajax } from "wizard/lib/ajax"; export default Ember.Object.extend(ValidState, { id: null, - @computed("index") displayIndex: index => index + 1, + @computed("index") + displayIndex: index => index + 1, @computed("fields.[]") fieldsById(fields) { diff --git a/app/assets/javascripts/wizard/models/wizard.js.es6 b/app/assets/javascripts/wizard/models/wizard.js.es6 index 95fa48bfb0..d83ba58ef0 100644 --- a/app/assets/javascripts/wizard/models/wizard.js.es6 +++ b/app/assets/javascripts/wizard/models/wizard.js.es6 @@ -4,7 +4,8 @@ import { ajax } from "wizard/lib/ajax"; import computed from "ember-addons/ember-computed-decorators"; const Wizard = Ember.Object.extend({ - @computed("steps.length") totalSteps: length => length, + @computed("steps.length") + totalSteps: length => length, getTitle() { const titleStep = this.get("steps").findBy("id", "forum-title"); diff --git a/package.json b/package.json index 0fc51ebb72..2f0744c03d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "chrome-launcher": "^0.10.2", "chrome-remote-interface": "^0.25.6", "eslint": "^4.19.1", - "prettier": "^1.13.0", + "prettier": "^1.14.0", "puppeteer": "^1.4.0" } } diff --git a/yarn.lock b/yarn.lock index d908e1a096..ae7b736cce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -813,9 +813,9 @@ prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" -prettier@1.13.4: - version "1.13.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.4.tgz#31bbae6990f13b1093187c731766a14036fa72e6" +prettier@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.0.tgz#847c235522035fd988100f1f43cf20a7d24f9372" process-nextick-args@~1.0.6: version "1.0.7" From 536f88b95ba530a95f410b431189b35629a6ba2d Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 30 Jul 2018 11:52:05 -0400 Subject: [PATCH 052/168] Revert "DEV: prettier 1.14.0" This reverts commit 1f899bec2103ade0dd40b65f986ae0ffc81cadf8. --- .../javascripts/admin/models/admin-user.js.es6 | 9 +++------ .../discourse/components/backup-codes.js.es6 | 3 +-- .../discourse/components/d-navigation.js.es6 | 3 +-- .../discourse/components/group-post.js.es6 | 3 +-- .../discourse/components/topic-entrance.js.es6 | 6 ++---- .../discourse/controllers/composer.js.es6 | 3 +-- .../javascripts/discourse/controllers/static.js.es6 | 3 +-- .../discourse/controllers/upload-selector.js.es6 | 6 ++---- .../javascripts/discourse/models/composer.js.es6 | 6 ++---- .../javascripts/discourse/models/nav-item.js.es6 | 3 +-- app/assets/javascripts/discourse/models/user.js.es6 | 12 ++++-------- .../javascripts/wizard/components/staff-count.js.es6 | 3 +-- .../wizard/components/wizard-step-form.js.es6 | 3 +-- .../javascripts/wizard/components/wizard-step.js.es6 | 6 ++---- .../javascripts/wizard/mixins/valid-state.js.es6 | 9 +++------ app/assets/javascripts/wizard/models/step.js.es6 | 3 +-- app/assets/javascripts/wizard/models/wizard.js.es6 | 3 +-- package.json | 2 +- yarn.lock | 6 +++--- 19 files changed, 32 insertions(+), 60 deletions(-) diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index ec230ffff4..39a2b7ee1d 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -530,14 +530,11 @@ const AdminUser = Discourse.User.extend({ } }, - @computed("suspended_by") - suspendedBy: wrapAdmin, + @computed("suspended_by") suspendedBy: wrapAdmin, - @computed("silenced_by") - silencedBy: wrapAdmin, + @computed("silenced_by") silencedBy: wrapAdmin, - @computed("approved_by") - approvedBy: wrapAdmin + @computed("approved_by") approvedBy: wrapAdmin }); AdminUser.reopenClass({ diff --git a/app/assets/javascripts/discourse/components/backup-codes.js.es6 b/app/assets/javascripts/discourse/components/backup-codes.js.es6 index 3df4b064b9..11457eb736 100644 --- a/app/assets/javascripts/discourse/components/backup-codes.js.es6 +++ b/app/assets/javascripts/discourse/components/backup-codes.js.es6 @@ -31,8 +31,7 @@ export default Ember.Component.extend({ } }, - @computed("formattedBackupCodes") - base64BackupCode: b64EncodeUnicode, + @computed("formattedBackupCodes") base64BackupCode: b64EncodeUnicode, @computed("backupCodes") formattedBackupCodes(backupCodes) { diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 index dc1eee1c3c..0ba2a85201 100644 --- a/app/assets/javascripts/discourse/components/d-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -18,8 +18,7 @@ export default Ember.Component.extend({ return hasDraft ? "topic.open_draft" : "topic.create"; }, - @computed("category.can_edit") - showCategoryEdit: canEdit => canEdit, + @computed("category.can_edit") showCategoryEdit: canEdit => canEdit, @computed("filterMode", "category", "noSubcategories") navItems(filterMode, category, noSubcategories) { diff --git a/app/assets/javascripts/discourse/components/group-post.js.es6 b/app/assets/javascripts/discourse/components/group-post.js.es6 index f94f36dc8d..5a28daf92d 100644 --- a/app/assets/javascripts/discourse/components/group-post.js.es6 +++ b/app/assets/javascripts/discourse/components/group-post.js.es6 @@ -1,6 +1,5 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("post.url") - postUrl: Discourse.getURL + @computed("post.url") postUrl: Discourse.getURL }); diff --git a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 index 216f024fc6..997171aa97 100644 --- a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 @@ -32,11 +32,9 @@ export default Ember.Component.extend(CleansUp, { topic: null, visible: null, - @computed("topic.created_at") - createdDate: createdAt => new Date(createdAt), + @computed("topic.created_at") createdDate: createdAt => new Date(createdAt), - @computed("topic.bumped_at") - bumpedDate: bumpedAt => new Date(bumpedAt), + @computed("topic.bumped_at") bumpedDate: bumpedAt => new Date(bumpedAt), @computed("createdDate", "bumpedDate") showTime(createdDate, bumpedDate) { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index f3f2675490..870f60b854 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -287,8 +287,7 @@ export default Ember.Controller.extend({ return authorizesOneOrMoreExtensions(); }, - @computed() - uploadIcon: () => uploadIcon(), + @computed() uploadIcon: () => uploadIcon(), actions: { cancelUpload() { diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index 8238d5fad9..6d3cbd2df1 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -7,8 +7,7 @@ export default Ember.Controller.extend({ showLoginButton: Em.computed.equal("model.path", "login"), - @computed("model.path") - bodyClass: path => `static-${path}`, + @computed("model.path") bodyClass: path => `static-${path}`, @computed("model.path") showSignupButton() { diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index b22674657b..918128d0c2 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -25,11 +25,9 @@ export default Ember.Controller.extend(ModalFunctionality, { remote: Ember.computed.equal("selection", "remote"), selection: "local", - @computed() - uploadIcon: () => uploadIcon(), + @computed() uploadIcon: () => uploadIcon(), - @computed() - title: () => uploadTranslate("title"), + @computed() title: () => uploadTranslate("title"), @computed("selection") tip(selection) { diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 801b44c24c..753c9bf741 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -75,8 +75,7 @@ const Composer = RestModel.extend({ return this.site.get("archetypes"); }.property(), - @computed("action") - sharedDraft: action => action === CREATE_SHARED_DRAFT, + @computed("action") sharedDraft: action => action === CREATE_SHARED_DRAFT, @computed categoryId: { @@ -134,8 +133,7 @@ const Composer = RestModel.extend({ topicFirstPost: Em.computed.or("creatingTopic", "editingFirstPost"), - @computed("action") - editingPost: isEdit, + @computed("action") editingPost: isEdit, replyingToTopic: Em.computed.equal("action", REPLY), diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index 6a9a782aea..ad4c0b1695 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -105,8 +105,7 @@ const NavItem = Discourse.Model.extend({ }); const ExtraNavItem = NavItem.extend({ - @computed("href") - href: href => href, + @computed("href") href: href => href, customFilter: null }); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 94f972ca27..74f103e551 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -198,17 +198,13 @@ const User = RestModel.extend({ return suspendedTill && moment(suspendedTill).isAfter(); }, - @computed("suspended_till") - suspendedForever: isForever, + @computed("suspended_till") suspendedForever: isForever, - @computed("silenced_till") - silencedForever: isForever, + @computed("silenced_till") silencedForever: isForever, - @computed("suspended_till") - suspendedTillDate: longDate, + @computed("suspended_till") suspendedTillDate: longDate, - @computed("silenced_till") - silencedTillDate: longDate, + @computed("silenced_till") silencedTillDate: longDate, changeUsername(new_username) { return ajax( diff --git a/app/assets/javascripts/wizard/components/staff-count.js.es6 b/app/assets/javascripts/wizard/components/staff-count.js.es6 index 7677d999de..17df6043d1 100644 --- a/app/assets/javascripts/wizard/components/staff-count.js.es6 +++ b/app/assets/javascripts/wizard/components/staff-count.js.es6 @@ -1,6 +1,5 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("field.value") - showStaffCount: staffCount => staffCount > 1 + @computed("field.value") showStaffCount: staffCount => staffCount > 1 }); diff --git a/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 b/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 index a60d5b39cb..ff2919bebb 100644 --- a/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 @@ -3,6 +3,5 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ classNameBindings: [":wizard-step-form", "customStepClass"], - @computed("step.id") - customStepClass: stepId => `wizard-step-${stepId}` + @computed("step.id") customStepClass: stepId => `wizard-step-${stepId}` }); diff --git a/app/assets/javascripts/wizard/components/wizard-step.js.es6 b/app/assets/javascripts/wizard/components/wizard-step.js.es6 index 4732548e8d..d5fb39af76 100644 --- a/app/assets/javascripts/wizard/components/wizard-step.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step.js.es6 @@ -31,8 +31,7 @@ export default Ember.Component.extend({ this.autoFocus(); }, - @computed("step.index") - showQuitButton: index => index === 0, + @computed("step.index") showQuitButton: index => index === 0, @computed("step.displayIndex", "wizard.totalSteps") showNextButton: (current, total) => current < total, @@ -40,8 +39,7 @@ export default Ember.Component.extend({ @computed("step.displayIndex", "wizard.totalSteps") showDoneButton: (current, total) => current === total, - @computed("step.index") - showBackButton: index => index > 0, + @computed("step.index") showBackButton: index => index > 0, @computed("step.banner") bannerImage(src) { diff --git a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 index ee00842eac..cd3faa5e69 100644 --- a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 +++ b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 @@ -15,14 +15,11 @@ export default { this.set("_validState", States.UNCHECKED); }, - @computed("_validState") - valid: state => state === States.VALID, + @computed("_validState") valid: state => state === States.VALID, - @computed("_validState") - invalid: state => state === States.INVALID, + @computed("_validState") invalid: state => state === States.INVALID, - @computed("_validState") - unchecked: state => state === States.UNCHECKED, + @computed("_validState") unchecked: state => state === States.UNCHECKED, setValid(valid, description) { this.set("_validState", valid ? States.VALID : States.INVALID); diff --git a/app/assets/javascripts/wizard/models/step.js.es6 b/app/assets/javascripts/wizard/models/step.js.es6 index d465b553b4..fc1d0ac1f5 100644 --- a/app/assets/javascripts/wizard/models/step.js.es6 +++ b/app/assets/javascripts/wizard/models/step.js.es6 @@ -5,8 +5,7 @@ import { ajax } from "wizard/lib/ajax"; export default Ember.Object.extend(ValidState, { id: null, - @computed("index") - displayIndex: index => index + 1, + @computed("index") displayIndex: index => index + 1, @computed("fields.[]") fieldsById(fields) { diff --git a/app/assets/javascripts/wizard/models/wizard.js.es6 b/app/assets/javascripts/wizard/models/wizard.js.es6 index d83ba58ef0..95fa48bfb0 100644 --- a/app/assets/javascripts/wizard/models/wizard.js.es6 +++ b/app/assets/javascripts/wizard/models/wizard.js.es6 @@ -4,8 +4,7 @@ import { ajax } from "wizard/lib/ajax"; import computed from "ember-addons/ember-computed-decorators"; const Wizard = Ember.Object.extend({ - @computed("steps.length") - totalSteps: length => length, + @computed("steps.length") totalSteps: length => length, getTitle() { const titleStep = this.get("steps").findBy("id", "forum-title"); diff --git a/package.json b/package.json index 2f0744c03d..0fc51ebb72 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "chrome-launcher": "^0.10.2", "chrome-remote-interface": "^0.25.6", "eslint": "^4.19.1", - "prettier": "^1.14.0", + "prettier": "^1.13.0", "puppeteer": "^1.4.0" } } diff --git a/yarn.lock b/yarn.lock index ae7b736cce..d908e1a096 100644 --- a/yarn.lock +++ b/yarn.lock @@ -813,9 +813,9 @@ prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" -prettier@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.0.tgz#847c235522035fd988100f1f43cf20a7d24f9372" +prettier@1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.4.tgz#31bbae6990f13b1093187c731766a14036fa72e6" process-nextick-args@~1.0.6: version "1.0.7" From a327393651831fdae4ee7cd60bf699789b760a8f Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 30 Jul 2018 11:56:48 -0400 Subject: [PATCH 053/168] fix 1.14.0 prettier offenses --- .../javascripts/admin/models/admin-user.js.es6 | 9 ++++++--- .../discourse/components/backup-codes.js.es6 | 3 ++- .../discourse/components/d-navigation.js.es6 | 3 ++- .../discourse/components/group-post.js.es6 | 3 ++- .../discourse/components/topic-entrance.js.es6 | 6 ++++-- .../discourse/controllers/composer.js.es6 | 3 ++- .../javascripts/discourse/controllers/static.js.es6 | 3 ++- .../discourse/controllers/upload-selector.js.es6 | 6 ++++-- .../javascripts/discourse/models/composer.js.es6 | 6 ++++-- .../javascripts/discourse/models/nav-item.js.es6 | 3 ++- app/assets/javascripts/discourse/models/user.js.es6 | 12 ++++++++---- .../javascripts/wizard/components/staff-count.js.es6 | 3 ++- .../wizard/components/wizard-step-form.js.es6 | 3 ++- .../javascripts/wizard/components/wizard-step.js.es6 | 6 ++++-- .../javascripts/wizard/mixins/valid-state.js.es6 | 9 ++++++--- app/assets/javascripts/wizard/models/step.js.es6 | 3 ++- app/assets/javascripts/wizard/models/wizard.js.es6 | 3 ++- 17 files changed, 56 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 39a2b7ee1d..ec230ffff4 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -530,11 +530,14 @@ const AdminUser = Discourse.User.extend({ } }, - @computed("suspended_by") suspendedBy: wrapAdmin, + @computed("suspended_by") + suspendedBy: wrapAdmin, - @computed("silenced_by") silencedBy: wrapAdmin, + @computed("silenced_by") + silencedBy: wrapAdmin, - @computed("approved_by") approvedBy: wrapAdmin + @computed("approved_by") + approvedBy: wrapAdmin }); AdminUser.reopenClass({ diff --git a/app/assets/javascripts/discourse/components/backup-codes.js.es6 b/app/assets/javascripts/discourse/components/backup-codes.js.es6 index 11457eb736..3df4b064b9 100644 --- a/app/assets/javascripts/discourse/components/backup-codes.js.es6 +++ b/app/assets/javascripts/discourse/components/backup-codes.js.es6 @@ -31,7 +31,8 @@ export default Ember.Component.extend({ } }, - @computed("formattedBackupCodes") base64BackupCode: b64EncodeUnicode, + @computed("formattedBackupCodes") + base64BackupCode: b64EncodeUnicode, @computed("backupCodes") formattedBackupCodes(backupCodes) { diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 index 0ba2a85201..dc1eee1c3c 100644 --- a/app/assets/javascripts/discourse/components/d-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -18,7 +18,8 @@ export default Ember.Component.extend({ return hasDraft ? "topic.open_draft" : "topic.create"; }, - @computed("category.can_edit") showCategoryEdit: canEdit => canEdit, + @computed("category.can_edit") + showCategoryEdit: canEdit => canEdit, @computed("filterMode", "category", "noSubcategories") navItems(filterMode, category, noSubcategories) { diff --git a/app/assets/javascripts/discourse/components/group-post.js.es6 b/app/assets/javascripts/discourse/components/group-post.js.es6 index 5a28daf92d..f94f36dc8d 100644 --- a/app/assets/javascripts/discourse/components/group-post.js.es6 +++ b/app/assets/javascripts/discourse/components/group-post.js.es6 @@ -1,5 +1,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("post.url") postUrl: Discourse.getURL + @computed("post.url") + postUrl: Discourse.getURL }); diff --git a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 index 997171aa97..216f024fc6 100644 --- a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 @@ -32,9 +32,11 @@ export default Ember.Component.extend(CleansUp, { topic: null, visible: null, - @computed("topic.created_at") createdDate: createdAt => new Date(createdAt), + @computed("topic.created_at") + createdDate: createdAt => new Date(createdAt), - @computed("topic.bumped_at") bumpedDate: bumpedAt => new Date(bumpedAt), + @computed("topic.bumped_at") + bumpedDate: bumpedAt => new Date(bumpedAt), @computed("createdDate", "bumpedDate") showTime(createdDate, bumpedDate) { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 870f60b854..f3f2675490 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -287,7 +287,8 @@ export default Ember.Controller.extend({ return authorizesOneOrMoreExtensions(); }, - @computed() uploadIcon: () => uploadIcon(), + @computed() + uploadIcon: () => uploadIcon(), actions: { cancelUpload() { diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index 6d3cbd2df1..8238d5fad9 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -7,7 +7,8 @@ export default Ember.Controller.extend({ showLoginButton: Em.computed.equal("model.path", "login"), - @computed("model.path") bodyClass: path => `static-${path}`, + @computed("model.path") + bodyClass: path => `static-${path}`, @computed("model.path") showSignupButton() { diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index 918128d0c2..b22674657b 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -25,9 +25,11 @@ export default Ember.Controller.extend(ModalFunctionality, { remote: Ember.computed.equal("selection", "remote"), selection: "local", - @computed() uploadIcon: () => uploadIcon(), + @computed() + uploadIcon: () => uploadIcon(), - @computed() title: () => uploadTranslate("title"), + @computed() + title: () => uploadTranslate("title"), @computed("selection") tip(selection) { diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 753c9bf741..801b44c24c 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -75,7 +75,8 @@ const Composer = RestModel.extend({ return this.site.get("archetypes"); }.property(), - @computed("action") sharedDraft: action => action === CREATE_SHARED_DRAFT, + @computed("action") + sharedDraft: action => action === CREATE_SHARED_DRAFT, @computed categoryId: { @@ -133,7 +134,8 @@ const Composer = RestModel.extend({ topicFirstPost: Em.computed.or("creatingTopic", "editingFirstPost"), - @computed("action") editingPost: isEdit, + @computed("action") + editingPost: isEdit, replyingToTopic: Em.computed.equal("action", REPLY), diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index ad4c0b1695..6a9a782aea 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -105,7 +105,8 @@ const NavItem = Discourse.Model.extend({ }); const ExtraNavItem = NavItem.extend({ - @computed("href") href: href => href, + @computed("href") + href: href => href, customFilter: null }); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 74f103e551..94f972ca27 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -198,13 +198,17 @@ const User = RestModel.extend({ return suspendedTill && moment(suspendedTill).isAfter(); }, - @computed("suspended_till") suspendedForever: isForever, + @computed("suspended_till") + suspendedForever: isForever, - @computed("silenced_till") silencedForever: isForever, + @computed("silenced_till") + silencedForever: isForever, - @computed("suspended_till") suspendedTillDate: longDate, + @computed("suspended_till") + suspendedTillDate: longDate, - @computed("silenced_till") silencedTillDate: longDate, + @computed("silenced_till") + silencedTillDate: longDate, changeUsername(new_username) { return ajax( diff --git a/app/assets/javascripts/wizard/components/staff-count.js.es6 b/app/assets/javascripts/wizard/components/staff-count.js.es6 index 17df6043d1..7677d999de 100644 --- a/app/assets/javascripts/wizard/components/staff-count.js.es6 +++ b/app/assets/javascripts/wizard/components/staff-count.js.es6 @@ -1,5 +1,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("field.value") showStaffCount: staffCount => staffCount > 1 + @computed("field.value") + showStaffCount: staffCount => staffCount > 1 }); diff --git a/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 b/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 index ff2919bebb..a60d5b39cb 100644 --- a/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step-form.js.es6 @@ -3,5 +3,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ classNameBindings: [":wizard-step-form", "customStepClass"], - @computed("step.id") customStepClass: stepId => `wizard-step-${stepId}` + @computed("step.id") + customStepClass: stepId => `wizard-step-${stepId}` }); diff --git a/app/assets/javascripts/wizard/components/wizard-step.js.es6 b/app/assets/javascripts/wizard/components/wizard-step.js.es6 index d5fb39af76..4732548e8d 100644 --- a/app/assets/javascripts/wizard/components/wizard-step.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step.js.es6 @@ -31,7 +31,8 @@ export default Ember.Component.extend({ this.autoFocus(); }, - @computed("step.index") showQuitButton: index => index === 0, + @computed("step.index") + showQuitButton: index => index === 0, @computed("step.displayIndex", "wizard.totalSteps") showNextButton: (current, total) => current < total, @@ -39,7 +40,8 @@ export default Ember.Component.extend({ @computed("step.displayIndex", "wizard.totalSteps") showDoneButton: (current, total) => current === total, - @computed("step.index") showBackButton: index => index > 0, + @computed("step.index") + showBackButton: index => index > 0, @computed("step.banner") bannerImage(src) { diff --git a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 index cd3faa5e69..ee00842eac 100644 --- a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 +++ b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 @@ -15,11 +15,14 @@ export default { this.set("_validState", States.UNCHECKED); }, - @computed("_validState") valid: state => state === States.VALID, + @computed("_validState") + valid: state => state === States.VALID, - @computed("_validState") invalid: state => state === States.INVALID, + @computed("_validState") + invalid: state => state === States.INVALID, - @computed("_validState") unchecked: state => state === States.UNCHECKED, + @computed("_validState") + unchecked: state => state === States.UNCHECKED, setValid(valid, description) { this.set("_validState", valid ? States.VALID : States.INVALID); diff --git a/app/assets/javascripts/wizard/models/step.js.es6 b/app/assets/javascripts/wizard/models/step.js.es6 index fc1d0ac1f5..d465b553b4 100644 --- a/app/assets/javascripts/wizard/models/step.js.es6 +++ b/app/assets/javascripts/wizard/models/step.js.es6 @@ -5,7 +5,8 @@ import { ajax } from "wizard/lib/ajax"; export default Ember.Object.extend(ValidState, { id: null, - @computed("index") displayIndex: index => index + 1, + @computed("index") + displayIndex: index => index + 1, @computed("fields.[]") fieldsById(fields) { diff --git a/app/assets/javascripts/wizard/models/wizard.js.es6 b/app/assets/javascripts/wizard/models/wizard.js.es6 index 95fa48bfb0..d83ba58ef0 100644 --- a/app/assets/javascripts/wizard/models/wizard.js.es6 +++ b/app/assets/javascripts/wizard/models/wizard.js.es6 @@ -4,7 +4,8 @@ import { ajax } from "wizard/lib/ajax"; import computed from "ember-addons/ember-computed-decorators"; const Wizard = Ember.Object.extend({ - @computed("steps.length") totalSteps: length => length, + @computed("steps.length") + totalSteps: length => length, getTitle() { const titleStep = this.get("steps").findBy("id", "forum-title"); From 2c90a2e5c25a0ee5090bb0d03e21e1bf3fc0c91c Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 30 Jul 2018 13:03:40 -0400 Subject: [PATCH 054/168] FIX: prevents mini-tag-chooser to catch unwanted focus --- .../select-kit/components/mini-tag-chooser.js.es6 | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 3e8edebc1d..882447346d 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -48,19 +48,15 @@ export default ComboBox.extend(Tags, { willDestroyElement() { this._super(...arguments); - $(".selected-name").off("touchend.select-kit pointerup.select-kit"); + this.$(".selected-name").off("touchend.select-kit pointerup.select-kit"); }, didInsertElement() { this._super(...arguments); - $(".selected-name").on( + this.$(".selected-name").on( "touchend.select-kit pointerup.select-kit", event => { - if (!this.get("isExpanded")) { - this.expand(event); - } - this.focusFilterOrHeader(); } ); From c6fd506a15be1d7c53486576e8b59db33af8c442 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 30 Jul 2018 13:10:20 -0400 Subject: [PATCH 055/168] fix eslint --- .../javascripts/select-kit/components/mini-tag-chooser.js.es6 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 882447346d..3fc1065df5 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -56,9 +56,7 @@ export default ComboBox.extend(Tags, { this.$(".selected-name").on( "touchend.select-kit pointerup.select-kit", - event => { - this.focusFilterOrHeader(); - } + () => this.focusFilterOrHeader() ); }, From 7368dd8e7d4fd9eb23207ff8dbb2a675fe33e7ab Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 30 Jul 2018 14:40:26 -0400 Subject: [PATCH 056/168] Prevent activity-metrics stats from wrapping --- app/assets/stylesheets/common/admin/dashboard_next.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/common/admin/dashboard_next.scss b/app/assets/stylesheets/common/admin/dashboard_next.scss index 116e2068f7..5d716da08d 100644 --- a/app/assets/stylesheets/common/admin/dashboard_next.scss +++ b/app/assets/stylesheets/common/admin/dashboard_next.scss @@ -381,6 +381,7 @@ td.value { text-align: right; + white-space: nowrap; padding: 8px 21px 8px 8px; // accounting for negative right caret margin &:nth-of-type(2) { padding: 8px 12px 8px; From c1e62808ed37f88d690a6c8f517287936f7c5a86 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 31 Jul 2018 01:01:03 +0530 Subject: [PATCH 057/168] FIX: Top site categories are displayed in random order --- .../discourse/widgets/hamburger-menu.js.es6 | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index e8e2fed85c..0c361e7c1f 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -179,40 +179,36 @@ export default createWidget("hamburger-menu", { listCategories() { const maxCategoriesToDisplay = this.siteSettings .hamburger_menu_categories_count; - const categoriesByCount = this.site.get("categoriesByCount"); - let categories = categoriesByCount.slice(); + let categories = this.site.get("categoriesByCount"); if (this.currentUser) { - let unreadCategoryIds = []; - let topCategoryIds = this.currentUser.get("top_category_ids") || []; - let i = 0; + const allCategories = this.site + .get("categories") + .filter(c => c.notification_level !== NotificationLevels.MUTED); - categoriesByCount + categories = allCategories + .filter(c => c.get("newTopics") > 0 || c.get("unreadTopics") > 0) .sort((a, b) => { return ( b.get("newTopics") + b.get("unreadTopics") - (a.get("newTopics") + a.get("unreadTopics")) ); - }) - .forEach(c => { - if (c.get("newTopics") > 0 || c.get("unreadTopics") > 0) { - unreadCategoryIds.push(c.id); - } }); - categories = categories.filter( - c => c.notification_level !== NotificationLevels.MUTED - ); - - [...unreadCategoryIds, ...topCategoryIds].uniq().forEach(id => { - const category = categories.find(c => c.id === id); - if (category) { - categories = categories.filter(c => c.id !== id); - categories.splice(i, 0, category); - i += 1; + const topCategoryIds = this.currentUser.get("top_category_ids") || []; + topCategoryIds.forEach(id => { + const category = allCategories.find(c => c.id === id); + if (category && !categories.includes(category)) { + categories.push(category); } }); + + categories = categories.concat( + allCategories + .filter(c => !categories.includes(c)) + .sort((a, b) => b.topic_count - a.topic_count) + ); } const moreCount = categories.length - maxCategoriesToDisplay; From b852275ced285133a28bc02ced12ab5989cc3a86 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 31 Jul 2018 01:36:37 +0530 Subject: [PATCH 058/168] Test method for site categories random order fix --- test/javascripts/widgets/hamburger-menu-test.js.es6 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 index c50323bf18..c8eacfa0d7 100644 --- a/test/javascripts/widgets/hamburger-menu-test.js.es6 +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -148,6 +148,13 @@ widgetTest("top categories - anonymous", { const maximum = count <= maxCategoriesToDisplay ? count : maxCategoriesToDisplay; assert.equal(find(".category-link").length, maximum); + assert.equal( + find(".category-link .category-name").text(), + categoriesByCount + .slice(0, maxCategoriesToDisplay) + .map(c => c.name) + .join("") + ); } }); From 8f1db615db35a82b9adcbb001acf149e09ffa65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 30 Jul 2018 22:11:38 +0200 Subject: [PATCH 059/168] FIX: don't break restore if function does not exist --- lib/backup_restore/restorer.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index f3b7ffbe5a..56d37480fe 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -65,18 +65,22 @@ module BackupRestore BackupRestore.move_tables_between_schemas("public", "backup") - # This is a temp fix to allow restores to work again. @tgxworld is - # current working on a fix that namespaces functions created by Discourse - # so that we can alter the schema of those functions before restoring. + # This is a temp fix to allow restores to work again. + # @tgxworld is currently working on a fix that namespaces functions + # created by Discourse so that we can alter the schema of those + # functions before restoring. %w{ raise_email_logs_reply_key_readonly raise_email_logs_skipped_reason_readonly }.each do |function| - DB.exec(<<~SQL) - DROP FUNCTION IF EXISTS backup.#{function}; - ALTER FUNCTION public.#{function} - SET SCHEMA "backup"; - SQL + begin + DB.exec(<<~SQL) + DROP FUNCTION IF EXISTS backup.#{function}; + ALTER FUNCTION public.#{function} SET SCHEMA "backup"; + SQL + rescue PG::UndefinedFunction + # the function does not exist, no need to worry about this + end end @db_was_changed = true From c12a9279f6182df2ca72d200c324d97b0ea6c220 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 30 Jul 2018 16:44:52 -0400 Subject: [PATCH 060/168] post deleted notification regression because controller was agreeing with all flags too early --- app/controllers/admin/flags_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb index 43c6cf129f..8924cc9661 100644 --- a/app/controllers/admin/flags_controller.rb +++ b/app/controllers/admin/flags_controller.rb @@ -77,13 +77,14 @@ class Admin::FlagsController < Admin::AdminController delete_post = params[:action_on_post] == "delete" restore_post = params[:action_on_post] == "restore" - PostAction.agree_flags!(post, current_user, delete_post) - if delete_post + # PostDestroy calls PostAction.agree_flags! PostDestroyer.new(current_user, post).destroy elsif restore_post + PostAction.agree_flags!(post, current_user, delete_post) PostDestroyer.new(current_user, post).recover elsif !keep_post + PostAction.agree_flags!(post, current_user, delete_post) PostAction.hide_post!(post, post_action_type) end From fd29ecb91abc50592b7caa9c75faf5274aec5634 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 30 Jul 2018 16:45:35 -0400 Subject: [PATCH 061/168] UX: include a flag reason in the post-deleted-by-staff-because-of-flags message --- app/models/post.rb | 6 +++++- config/locales/server.en.yml | 2 ++ lib/post_destroyer.rb | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index a98cd17807..6953169c56 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -436,8 +436,12 @@ class Post < ActiveRecord::Base post_actions.where(post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil).count != 0 end + def active_flags + post_actions.active.where(post_action_type_id: PostActionType.flag_types_without_custom.values) + end + def has_active_flag? - post_actions.active.where(post_action_type_id: PostActionType.flag_types_without_custom.values).count != 0 + active_flags.count != 0 end def unhide! diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 8c85dc3f75..ae2ed3a40d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2253,6 +2253,8 @@ en: <%{base_url}%{url}> + %{flag_reason} + This post was flagged by the community and a staff member opted to remove it. Please review our [community guidelines](%{base_url}/guidelines) for details. diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 33c316007a..ff808b418f 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -201,7 +201,10 @@ class PostDestroyer :send_system_message, user_id: @post.user.id, message_type: :flags_agreed_and_post_deleted, - message_options: { url: @post.url } + message_options: { + url: @post.url, + flag_reason: I18n.t("flag_reasons.#{@post.active_flags.last.post_action_type.name_key}", locale: SiteSetting.default_locale) + } ) end From 09bb25a2878c650e61dcc94ba11aa041ba059d7a Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 30 Jul 2018 16:43:33 -0700 Subject: [PATCH 062/168] omit needless words --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3f5144742c..571fe203c0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -152,7 +152,7 @@ en: user_left: "%{who} removed themselves from this message %{when}" removed_user: "removed %{who} %{when}" removed_group: "removed %{who} %{when}" - autobumped: "automatically bumped topic %{when}" + autobumped: "automatically bumped %{when}" autoclosed: enabled: 'closed %{when}' disabled: 'opened %{when}' From bd40a6b7a0ecabb81bb6b5cdc3fd85694cd03e31 Mon Sep 17 00:00:00 2001 From: Rishabh <5862206+rishabhnambiar@users.noreply.github.com> Date: Tue, 31 Jul 2018 08:48:27 +0530 Subject: [PATCH 063/168] Add 'rake' to bundle install command Without installing rake, `bundle install` fails when trying to install `sassc`. --- docs/DEVELOPER-ADVANCED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index 1b21fa38df..9e0ac26a59 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -23,7 +23,7 @@ To get your Ubuntu 16.04 LTS install up and running to develop Discourse and Dis rvm install 2.5.1 rvm --default use 2.5.1 # If this error out check https://rvm.io/integration/gnome-terminal - gem install bundler mailcatcher + gem install bundler mailcatcher rake # Postgresql sudo -u postgres -i From ece3cb73dfecf65dad27eeb0eab95404edc002b7 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 31 Jul 2018 09:12:30 +0530 Subject: [PATCH 064/168] Rename humburger_menu_categories_count site setting to header_dropdown_category_count --- .../javascripts/discourse/widgets/hamburger-menu.js.es6 | 2 +- app/serializers/current_user_serializer.rb | 2 +- config/locales/server.en.yml | 2 +- config/site_settings.yml | 2 +- test/javascripts/widgets/hamburger-menu-test.js.es6 | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 0c361e7c1f..651cf97360 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -178,7 +178,7 @@ export default createWidget("hamburger-menu", { listCategories() { const maxCategoriesToDisplay = this.siteSettings - .hamburger_menu_categories_count; + .header_dropdown_category_count; let categories = this.site.get("categoriesByCount"); if (this.currentUser) { diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index c55d3ece8a..cd44c75f66 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -168,7 +168,7 @@ class CurrentUserSerializer < BasicUserSerializer WHEN notification_level = 4 THEN 3 END") .pluck(:category_id) - .slice(0, SiteSetting.hamburger_menu_categories_count) + .slice(0, SiteSetting.header_dropdown_category_count) end def dismissed_banner_key diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ae2ed3a40d..067f43833c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1655,7 +1655,7 @@ en: suppress_uncategorized_badge: "Don't show the badge for uncategorized topics in topic lists." - hamburger_menu_categories_count: "How many categories can be displayed in the hamburger menu." + header_dropdown_category_count: "How many categories can be displayed in the header dropdown menu." permalink_normalizations: "Apply the following regex before matching permalinks, for example: /(topic.*)\\?.*/\\1 will strip query strings from topic routes. Format is regex+string use \\1 etc. to access captures" diff --git a/config/site_settings.yml b/config/site_settings.yml index 1aabd59c5d..ee0b96c9cc 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1415,7 +1415,7 @@ uncategorized: client: true default: true - hamburger_menu_categories_count: + header_dropdown_category_count: client: true default: 8 diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 index c8eacfa0d7..e94233df47 100644 --- a/test/javascripts/widgets/hamburger-menu-test.js.es6 +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -138,8 +138,8 @@ widgetTest("top categories - anonymous", { anonymous: true, beforeEach() { - this.siteSettings.hamburger_menu_categories_count = 8; - maxCategoriesToDisplay = this.siteSettings.hamburger_menu_categories_count; + this.siteSettings.header_dropdown_category_count = 8; + maxCategoriesToDisplay = this.siteSettings.header_dropdown_category_count; categoriesByCount = this.site.get("categoriesByCount"); }, @@ -162,8 +162,8 @@ widgetTest("top categories", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { - this.siteSettings.hamburger_menu_categories_count = 8; - maxCategoriesToDisplay = this.siteSettings.hamburger_menu_categories_count; + this.siteSettings.header_dropdown_category_count = 8; + maxCategoriesToDisplay = this.siteSettings.header_dropdown_category_count; categoriesByCount = this.site.get("categoriesByCount").slice(); categoriesByCount.every(c => { if (!topCategoryIds.includes(c.id)) { From 9fe765bca79c78c3c6544f9fc48afb08b9b11dcd Mon Sep 17 00:00:00 2001 From: Mohammad AlTawil Date: Tue, 31 Jul 2018 07:41:49 +0300 Subject: [PATCH 065/168] Create transliterate.ar.yml (#6197) * Create transliterate.ar.yml * Add vowels and diphthongs --- config/locales/transliterate.ar.yml | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 config/locales/transliterate.ar.yml diff --git a/config/locales/transliterate.ar.yml b/config/locales/transliterate.ar.yml new file mode 100644 index 0000000000..4470b59cbc --- /dev/null +++ b/config/locales/transliterate.ar.yml @@ -0,0 +1,61 @@ +# encoding: utf-8 +# +# This file contains transliteration rules for Arabic +# +# To validate this YAML file after you change it, please paste it into +# http://yamllint.com/ + +ar: + i18n: + transliterate: + rule: + ء: e + آ: a + أ: a + ؤ: w + إ: i + ئ: "y" + ا: a + ب: b + ة: t + ت: t + ث: th + ج: j + ح: h + خ: kh + د: d + ذ: dh + ر: r + ز: z + س: s + ش: sh + ص: s + ض: d + ط: t + ظ: z + ع: e + غ: gh + ـ: _ + ف: f + ق: q + ك: k + ل: l + م: m + ن: "n" + ه: h + و: w + ى: a + ي: "y" + َ‎: a + ُ: u + ِ‎: i + ٠: 0 + ١: 1 + ٢: 2 + ٣: 3 + ٤: 4 + ٥: 5 + ٦: 6 + ٧: 7 + ٩: 9 + ۸: 8 From 64f533db99bca433b755bff9773dad9dbc5cae16 Mon Sep 17 00:00:00 2001 From: Mohammad AlTawil Date: Tue, 31 Jul 2018 07:43:16 +0300 Subject: [PATCH 066/168] Add display name to user (#6198) --- script/bulk_import/vbulletin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/script/bulk_import/vbulletin.rb b/script/bulk_import/vbulletin.rb index 830c330e93..bde627df94 100644 --- a/script/bulk_import/vbulletin.rb +++ b/script/bulk_import/vbulletin.rb @@ -97,6 +97,7 @@ class BulkImport::VBulletin < BulkImport::Base u = { imported_id: row[0], username: normalize_text(row[1]), + name: normalize_text(row[1]), created_at: Time.zone.at(row[3]), date_of_birth: parse_birthday(row[4]), primary_group_id: group_id_from_imported_id(row[6]), From b55d9e63a09f8dc8020b606d5a9f242758b1a785 Mon Sep 17 00:00:00 2001 From: Jay Pfaffman Date: Tue, 31 Jul 2018 06:45:59 +0200 Subject: [PATCH 067/168] Rake mail test debugging (#6171) * FEATURE: rake emails:test add debugging * Oops! Remove safety code * more language tweaks --- lib/tasks/emails.rake | 127 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 2 deletions(-) diff --git a/lib/tasks/emails.rake b/lib/tasks/emails.rake index c4258bd9d3..45e8dd9c25 100644 --- a/lib/tasks/emails.rake +++ b/lib/tasks/emails.rake @@ -55,9 +55,132 @@ task "emails:import" => :environment do end end -desc 'Send email test message' +desc "Check if SMTP connection is successful and send test message" task 'emails:test', [:email] => [:environment] do |_, args| email = args[:email] + message = "OK" + begin + smtp=Discourse::Application.config.action_mailer.smtp_settings - Email::Sender.new(TestMailer.send_test(email), :test_message).send + if smtp[:address].match(/smtp\.gmail\.com/) + puts " + +#{smtp} +============================== WARNING ============================== + +Sending mail with Gmail is a violation of their terms of service. + +Sending with G Suite might work, but it is not recommended. For information see: +https://meta.discourse.org/t/dscourse-aws-ec2-g-suite-troubleshoting/62931?u=pfaffman + +========================= CONTINUING TEST ============================ +" + end + + puts "Testing sending to #{email} using #{smtp[:user_name]}:#{smtp[:password]}@#{smtp[:address]}:#{smtp[:port]}." + + Net::SMTP.start(smtp[:address], smtp[:port]) + .auth_login(smtp[:user_name], smtp[:password]) + rescue Exception => e + + + + + + if e.to_s.match(/execution expired/) + message = " + +======================================== ERROR ======================================== +Connection to port #{ENV["DISCOURSE_SMTP_PORT"]} failed. +====================================== SOLUTION ======================================= +The most likely problem is that your server has outgoing SMTP traffic blocked. +If you are using a service like Mailgun or Sendgrid, try using port 2525. +======================================================================================= + +" + elsif e.to_s.match(/535/) + message = " + +======================================== ERROR ======================================== + AUTHENTICATION FAILED + +#{e} + +====================================== SOLUTION ======================================= +The most likely problem is that your SMTP username and/or Password is incorrect. +Check them and try again. +======================================================================================= + +" + elsif e.to_s.match(/Connection refused/) + message = " + +======================================== ERROR ======================================== + CONNECTION REFUSED + +#{e} + +====================================== SOLUTION ======================================= +The most likely problem is that you have chosen the wrong port or a network problem is +blocking access from the Docker container. + +Check the port and your networking configuration. +======================================================================================= + +" + elsif e.to_s.match(/service not known/) + message = " + +======================================== ERROR ======================================== + SMTP SERVER NOT FOUND + +#{e} + +====================================== SOLUTION ======================================= +The most likely problem is that the host name of your SMTP server is incorrect. +Check it and try again. +======================================================================================= + +" + else + message = " + +======================================== ERROR ======================================== + UNEXPECTED ERROR + +#{e} + +====================================== SOLUTION ======================================= +This is not a common error. No recommended solution exists! + +Please report the exact error message above. (And a solution, if you find one!) +======================================================================================= + +" + end + end + if message == "OK" + puts "SMTP server connection successful." + else + puts message + exit + end + begin + puts "Sending to #{email}. . . " + Email::Sender.new(TestMailer.send_test(email), :test_message).send + rescue + puts "Sending mail failed." + else + puts " +Mail accepted by SMTP server. + +If you do not receive the message, check your SPAM folder +or test again using a service like http://www.mail-tester.com/. + +If the message is not delivered it is not a problem with Discourse. + +Check the SMTP server logs to see why it failed to deliver the message. + +" + end end From f0c203a5cfb2fbe7430892bdb1ab1a150c6c07d3 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 31 Jul 2018 14:50:02 +1000 Subject: [PATCH 068/168] clean up previous commit --- lib/tasks/emails.rake | 136 +++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 74 deletions(-) diff --git a/lib/tasks/emails.rake b/lib/tasks/emails.rake index 45e8dd9c25..82bb6b6c83 100644 --- a/lib/tasks/emails.rake +++ b/lib/tasks/emails.rake @@ -60,21 +60,20 @@ task 'emails:test', [:email] => [:environment] do |_, args| email = args[:email] message = "OK" begin - smtp=Discourse::Application.config.action_mailer.smtp_settings + smtp = Discourse::Application.config.action_mailer.smtp_settings if smtp[:address].match(/smtp\.gmail\.com/) - puts " + puts <<~STR + #{smtp} + ============================== WARNING ============================== -#{smtp} -============================== WARNING ============================== + Sending mail with Gmail is a violation of their terms of service. -Sending mail with Gmail is a violation of their terms of service. + Sending with G Suite might work, but it is not recommended. For information see: + https://meta.discourse.org/t/dscourse-aws-ec2-g-suite-troubleshoting/62931?u=pfaffman -Sending with G Suite might work, but it is not recommended. For information see: -https://meta.discourse.org/t/dscourse-aws-ec2-g-suite-troubleshoting/62931?u=pfaffman - -========================= CONTINUING TEST ============================ -" + ========================= CONTINUING TEST ============================ + STR end puts "Testing sending to #{email} using #{smtp[:user_name]}:#{smtp[:password]}@#{smtp[:address]}:#{smtp[:port]}." @@ -83,80 +82,70 @@ https://meta.discourse.org/t/dscourse-aws-ec2-g-suite-troubleshoting/62931?u=pfa .auth_login(smtp[:user_name], smtp[:password]) rescue Exception => e - - - - if e.to_s.match(/execution expired/) - message = " - -======================================== ERROR ======================================== -Connection to port #{ENV["DISCOURSE_SMTP_PORT"]} failed. -====================================== SOLUTION ======================================= -The most likely problem is that your server has outgoing SMTP traffic blocked. -If you are using a service like Mailgun or Sendgrid, try using port 2525. -======================================================================================= - -" + message = <<~STR + ======================================== ERROR ======================================== + Connection to port #{ENV["DISCOURSE_SMTP_PORT"]} failed. + ====================================== SOLUTION ======================================= + The most likely problem is that your server has outgoing SMTP traffic blocked. + If you are using a service like Mailgun or Sendgrid, try using port 2525. + ======================================================================================= + STR elsif e.to_s.match(/535/) - message = " + message = <<~STR + ======================================== ERROR ======================================== + AUTHENTICATION FAILED -======================================== ERROR ======================================== - AUTHENTICATION FAILED + #{e} -#{e} - -====================================== SOLUTION ======================================= -The most likely problem is that your SMTP username and/or Password is incorrect. -Check them and try again. -======================================================================================= - -" + ====================================== SOLUTION ======================================= + The most likely problem is that your SMTP username and/or Password is incorrect. + Check them and try again. + ======================================================================================= + STR + j elsif e.to_s.match(/Connection refused/) - message = " + message = <<~STR + ======================================== ERROR ======================================== + CONNECTION REFUSED -======================================== ERROR ======================================== - CONNECTION REFUSED + #{e} -#{e} + ====================================== SOLUTION ======================================= + The most likely problem is that you have chosen the wrong port or a network problem is + blocking access from the Docker container. -====================================== SOLUTION ======================================= -The most likely problem is that you have chosen the wrong port or a network problem is -blocking access from the Docker container. + Check the port and your networking configuration. + ======================================================================================= + STR -Check the port and your networking configuration. -======================================================================================= - -" elsif e.to_s.match(/service not known/) - message = " + message = <<~STR + ======================================== ERROR ======================================== + SMTP SERVER NOT FOUND -======================================== ERROR ======================================== - SMTP SERVER NOT FOUND + #{e} -#{e} + ====================================== SOLUTION ======================================= + The most likely problem is that the host name of your SMTP server is incorrect. + Check it and try again. + ======================================================================================= + STR -====================================== SOLUTION ======================================= -The most likely problem is that the host name of your SMTP server is incorrect. -Check it and try again. -======================================================================================= - -" else - message = " -======================================== ERROR ======================================== - UNEXPECTED ERROR + message = <<~STR + ======================================== ERROR ======================================== + UNEXPECTED ERROR -#{e} + #{e} -====================================== SOLUTION ======================================= -This is not a common error. No recommended solution exists! + ====================================== SOLUTION ======================================= + This is not a common error. No recommended solution exists! -Please report the exact error message above. (And a solution, if you find one!) -======================================================================================= - -" + Please report the exact error message above. (And a solution, if you find one!) + ======================================================================================= + STR end end if message == "OK" @@ -171,16 +160,15 @@ Please report the exact error message above. (And a solution, if you find one!) rescue puts "Sending mail failed." else - puts " -Mail accepted by SMTP server. + puts <<~STR + Mail accepted by SMTP server. -If you do not receive the message, check your SPAM folder -or test again using a service like http://www.mail-tester.com/. + If you do not receive the message, check your SPAM folder + or test again using a service like http://www.mail-tester.com/. -If the message is not delivered it is not a problem with Discourse. + If the message is not delivered it is not a problem with Discourse. -Check the SMTP server logs to see why it failed to deliver the message. - -" + Check the SMTP server logs to see why it failed to deliver the message. + STR end end From afe3b00c0facdddb88e938ace7b1ee6825fb0121 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 31 Jul 2018 11:15:31 +0530 Subject: [PATCH 069/168] FIX: use hidden setting for max export file size --- config/site_settings.yml | 4 ++++ lib/validators/upload_validator.rb | 6 +++++- spec/components/validators/upload_validator_spec.rb | 9 +++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index ee0b96c9cc..aa34a64a8b 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -869,6 +869,10 @@ files: default: 40 min: 5 max: 150 + max_export_file_size_kb: + hidden: true + default: 50000 + max: 1024000 theme_authorized_extensions: default: 'jpg|jpeg|png|woff|woff2|svg|eot|ttf|otf|gif' type: list diff --git a/lib/validators/upload_validator.rb b/lib/validators/upload_validator.rb index 5b9da9793d..60074a197f 100644 --- a/lib/validators/upload_validator.rb +++ b/lib/validators/upload_validator.rb @@ -113,7 +113,11 @@ class Validators::UploadValidator < ActiveModel::Validator end def maximum_file_size(upload, type) - max_size_kb = SiteSetting.send("max_#{type}_size_kb") + max_size_kb = if upload.for_export + SiteSetting.max_export_file_size_kb + else + SiteSetting.send("max_#{type}_size_kb") + end max_size_bytes = max_size_kb.kilobytes if upload.filesize > max_size_bytes diff --git a/spec/components/validators/upload_validator_spec.rb b/spec/components/validators/upload_validator_spec.rb index 164d0b0a8e..f256e2f176 100644 --- a/spec/components/validators/upload_validator_spec.rb +++ b/spec/components/validators/upload_validator_spec.rb @@ -20,9 +20,14 @@ describe Validators::UploadValidator do it "allows 'gz' as extension when uploading export file" do SiteSetting.authorized_extensions = "" - created_upload = UploadCreator.new(csv_file, "#{filename}.gz", for_export: true).create_for(user.id) - expect(created_upload).to be_valid + expect(UploadCreator.new(csv_file, "#{filename}.gz", for_export: true).create_for(user.id)).to be_valid end + it "allows uses max_export_file_size_kb when uploading export file" do + SiteSetting.max_attachment_size_kb = "0" + SiteSetting.authorized_extensions = "gz" + + expect(UploadCreator.new(csv_file, "#{filename}.gz", for_export: true).create_for(user.id)).to be_valid + end end end From 458d9cd17ae506b903e4fb2c77c79be2d152ab8a Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 31 Jul 2018 22:52:03 +0530 Subject: [PATCH 070/168] bump onebox version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 3ec355c0a8..c2e9be001f 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.55' +gem 'onebox', '1.8.57' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 2118599739..6294c698ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -256,7 +256,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.55) + onebox (1.8.57) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -506,7 +506,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.55) + onebox (= 1.8.57) openid-redis-store pg pry-nav From b6bb8df6229ed18adf7ac6ce5ebd5b5c6e4226ef Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 31 Jul 2018 13:53:05 -0400 Subject: [PATCH 071/168] FIX: We loosened username restrictions some time ago Additionally, remove `Discourse.User` constant --- .../discourse/components/user-selector.js.es6 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index 3baf8066c7..f6a466f046 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -46,6 +46,11 @@ export default TextField.extend({ return usernames; } + + const termRegexp = (currentUser && currentUser.can_send_private_email_messages) ? + /[^\w.-@]/g : + /[^\w.-]/g; + this.$() .val(this.get("usernames")) .autocomplete({ @@ -56,14 +61,9 @@ export default TextField.extend({ updateData: opts && opts.updateData ? opts.updateData : false, dataSource(term) { - const termRegex = Discourse.User.currentProp( - "can_send_private_email_messages" - ) - ? /[^a-zA-Z0-9_\-\.@\+]/ - : /[^a-zA-Z0-9_\-\.]/; var results = userSearch({ - term: term.replace(termRegex, ""), + term: term.replace(termRegexp, ""), topicId: self.get("topicId"), exclude: excludedUsernames(), includeGroups, From 4b166cccc1e535a02317d13008a414eadb08a28b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 31 Jul 2018 14:19:45 -0400 Subject: [PATCH 072/168] FIX: Linting error --- .../discourse/components/user-selector.js.es6 | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index f6a466f046..2a6b4ceee4 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -46,10 +46,10 @@ export default TextField.extend({ return usernames; } - - const termRegexp = (currentUser && currentUser.can_send_private_email_messages) ? - /[^\w.-@]/g : - /[^\w.-]/g; + const termRegexp = + currentUser && currentUser.can_send_private_email_messages + ? /[^\w.-@]/g + : /[^\w.-]/g; this.$() .val(this.get("usernames")) @@ -61,7 +61,6 @@ export default TextField.extend({ updateData: opts && opts.updateData ? opts.updateData : false, dataSource(term) { - var results = userSearch({ term: term.replace(termRegexp, ""), topicId: self.get("topicId"), From 4ad7ce70ce03043c5149ea309e16362b8514dc1f Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 31 Jul 2018 17:12:55 -0400 Subject: [PATCH 073/168] REFACTOR: extract scheduler to the mini_scheduler gem --- Gemfile | 1 + Gemfile.lock | 2 + app/jobs/base.rb | 4 +- config/initializers/100-sidekiq.rb | 36 +- config/routes.rb | 2 +- lib/discourse.rb | 2 + lib/scheduler/manager.rb | 360 ------------------ lib/scheduler/schedule.rb | 37 -- lib/scheduler/schedule_info.rb | 138 ------- lib/scheduler/scheduler.rb | 7 - lib/scheduler/views/history.erb | 47 --- lib/scheduler/views/scheduler.erb | 73 ---- lib/scheduler/web.rb | 65 ---- lib/tasks/scheduler.rake | 2 +- spec/components/scheduler/manager_spec.rb | 255 ------------- .../scheduler/schedule_info_spec.rb | 103 ----- 16 files changed, 28 insertions(+), 1106 deletions(-) delete mode 100644 lib/scheduler/manager.rb delete mode 100644 lib/scheduler/schedule.rb delete mode 100644 lib/scheduler/schedule_info.rb delete mode 100644 lib/scheduler/scheduler.rb delete mode 100644 lib/scheduler/views/history.erb delete mode 100644 lib/scheduler/views/scheduler.erb delete mode 100644 lib/scheduler/web.rb delete mode 100644 spec/components/scheduler/manager_spec.rb delete mode 100644 spec/components/scheduler/schedule_info_spec.rb diff --git a/Gemfile b/Gemfile index 3ec355c0a8..fe489cb02c 100644 --- a/Gemfile +++ b/Gemfile @@ -88,6 +88,7 @@ gem 'thor', require: false gem 'rinku' gem 'sanitize' gem 'sidekiq' +gem 'mini_scheduler' # for sidekiq web gem 'tilt', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 2118599739..f9b8ddc561 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,6 +200,7 @@ GEM mini_portile2 (2.3.0) mini_racer (0.2.0) libv8 (>= 6.3) + mini_scheduler (0.8.1) mini_sql (0.1.10) mini_suffix (0.3.0) ffi (~> 1.9) @@ -489,6 +490,7 @@ DEPENDENCIES message_bus mini_mime mini_racer + mini_scheduler mini_sql mini_suffix minitest diff --git a/app/jobs/base.rb b/app/jobs/base.rb index fa16e3179b..1828114513 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -1,5 +1,3 @@ -require 'scheduler/scheduler' - module Jobs def self.queued @@ -173,7 +171,7 @@ module Jobs end class Scheduled < Base - extend Scheduler::Schedule + extend MiniScheduler::Schedule def perform(*args) return if Discourse.readonly_mode? diff --git a/config/initializers/100-sidekiq.rb b/config/initializers/100-sidekiq.rb index f361c9fdee..64b24e1f51 100644 --- a/config/initializers/100-sidekiq.rb +++ b/config/initializers/100-sidekiq.rb @@ -1,4 +1,5 @@ require "sidekiq/pausable" +require "sidekiq/web" Sidekiq.configure_client do |config| config.redis = Discourse.sidekiq_redis_config @@ -12,6 +13,24 @@ Sidekiq.configure_server do |config| end end +MiniScheduler.configure do |config| + + config.redis = $redis + + config.job_exception_handler do |ex, context| + Discourse.handle_job_exception(ex, context) + end + + config.job_ran do |stat| + DiscourseEvent.trigger(:scheduled_job_ran, stat) + end + + config.before_sidekiq_web_request do + RailsMultisite::ConnectionManagement.establish_connection(db: 'default') + end + +end + if Sidekiq.server? # defer queue should simply run in sidekiq Scheduler::Defer.async = false @@ -27,22 +46,7 @@ if Sidekiq.server? scheduler_hostname = ENV["UNICORN_SCHEDULER_HOSTNAME"] if !scheduler_hostname || scheduler_hostname.split(',').include?(`hostname`.strip) - require 'scheduler/scheduler' - manager = Scheduler::Manager.new($redis.without_namespace) - Scheduler::Manager.discover_schedules.each do |schedule| - manager.ensure_schedule!(schedule) - end - Thread.new do - while true - begin - manager.tick - rescue => e - # the show must go on - Discourse.handle_job_exception(e, message: "While ticking scheduling manager") - end - sleep 1 - end - end + MiniScheduler.start end end end diff --git a/config/routes.rb b/config/routes.rb index a9c0aa4a1a..c140d73538 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@ require "sidekiq/web" -require_dependency "scheduler/web" +require "mini_scheduler/web" require_dependency "admin_constraint" require_dependency "staff_constraint" require_dependency "homepage_constraint" diff --git a/lib/discourse.rb b/lib/discourse.rb index 9160e887f0..a00aaea1e4 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -43,6 +43,8 @@ module Discourse # other desired context. # See app/jobs/base.rb for the error_context function. def self.handle_job_exception(ex, context = {}, parent_logger = nil) + return if ex.class == Jobs::HandledExceptionWrapper + context ||= {} parent_logger ||= SidekiqExceptionHandler diff --git a/lib/scheduler/manager.rb b/lib/scheduler/manager.rb deleted file mode 100644 index e9d98b9abd..0000000000 --- a/lib/scheduler/manager.rb +++ /dev/null @@ -1,360 +0,0 @@ -# Initially we used sidetiq, this was a problem: -# -# 1. No mechnism to add "randomisation" into job execution -# 2. No stats about previous runs or failures -# 3. Dependency on ice_cube gem causes runaway CPU - -require_dependency 'distributed_mutex' - -module Scheduler - class Manager - attr_accessor :random_ratio, :redis, :enable_stats - - class Runner - def initialize(manager) - @stopped = false - @mutex = Mutex.new - @queue = Queue.new - @manager = manager - @reschedule_orphans_thread = Thread.new do - while !@stopped - sleep 1.minute - @mutex.synchronize do - reschedule_orphans - end - end - end - @keep_alive_thread = Thread.new do - while !@stopped - @mutex.synchronize do - keep_alive - end - sleep (@manager.keep_alive_duration / 2) - end - end - @thread = Thread.new do - while !@stopped - process_queue - end - end - end - - def keep_alive - @manager.keep_alive - rescue => ex - Discourse.handle_job_exception(ex, message: "Scheduling manager keep-alive") - end - - def reschedule_orphans - @manager.reschedule_orphans! - rescue => ex - Discourse.handle_job_exception(ex, message: "Scheduling manager orphan rescheduler") - end - - def hostname - @hostname ||= begin - `hostname` - rescue - "unknown" - end - end - - def process_queue - - klass = @queue.deq - return unless klass - - # hack alert, I need to both deq and set @running atomically. - @running = true - failed = false - start = Time.now.to_f - info = @mutex.synchronize { @manager.schedule_info(klass) } - stat = nil - error = nil - - begin - info.prev_result = "RUNNING" - @mutex.synchronize { info.write! } - - if @manager.enable_stats - RailsMultisite::ConnectionManagement.with_connection("default") do - stat = SchedulerStat.create!( - name: klass.to_s, - hostname: hostname, - pid: Process.pid, - started_at: Time.zone.now, - live_slots_start: GC.stat[:heap_live_slots] - ) - end - end - - klass.new.perform - rescue => e - if e.class != Jobs::HandledExceptionWrapper - Discourse.handle_job_exception(e, message: "Running a scheduled job", job: klass) - end - - error = "#{e.class}: #{e.message} #{e.backtrace.join("\n")}" - failed = true - end - duration = ((Time.now.to_f - start) * 1000).to_i - info.prev_duration = duration - info.prev_result = failed ? "FAILED" : "OK" - info.current_owner = nil - if stat - RailsMultisite::ConnectionManagement.with_connection("default") do - stat.update!( - duration_ms: duration, - live_slots_finish: GC.stat[:heap_live_slots], - success: !failed, - error: error - ) - DiscourseEvent.trigger(:scheduled_job_ran, stat) - end - end - attempts(3) do - @mutex.synchronize { info.write! } - end - rescue => ex - Discourse.handle_job_exception(ex, message: "Processing scheduled job queue") - ensure - @running = false - ActiveRecord::Base.connection_handler.clear_active_connections! - end - - def stop! - return if @stopped - - @mutex.synchronize do - @stopped = true - - @keep_alive_thread.kill - @reschedule_orphans_thread.kill - - @keep_alive_thread.join - @reschedule_orphans_thread.join - - enq(nil) - - kill_thread = Thread.new do - sleep 0.5 - @thread.kill - end - - @thread.join - kill_thread.kill - kill_thread.join - end - end - - def enq(klass) - @queue << klass - end - - def wait_till_done - while !@queue.empty? && !(@queue.num_waiting > 0) - sleep 0.001 - end - # this is a hack, but is only used for test anyway - sleep 0.001 - while @running - sleep 0.001 - end - end - - def attempts(n) - n.times { - begin - yield; break - rescue - sleep Random.rand - end - } - end - - end - - def self.without_runner(redis = nil) - self.new(redis, skip_runner: true) - end - - def initialize(redis = nil, options = nil) - @redis = $redis || redis - @random_ratio = 0.1 - unless options && options[:skip_runner] - @runner = Runner.new(self) - self.class.current = self - end - - @hostname = options && options[:hostname] - @manager_id = SecureRandom.hex - - if options && options.key?(:enable_stats) - @enable_stats = options[:enable_stats] - else - @enable_stats = true - end - end - - def self.current - @current - end - - def self.current=(manager) - @current = manager - end - - def hostname - @hostname ||= `hostname`.strip - end - - def schedule_info(klass) - ScheduleInfo.new(klass, self) - end - - def next_run(klass) - schedule_info(klass).next_run - end - - def ensure_schedule!(klass) - lock do - schedule_info(klass).schedule! - end - end - - def remove(klass) - lock do - schedule_info(klass).del! - end - end - - def reschedule_orphans! - lock do - reschedule_orphans_on! - reschedule_orphans_on!(hostname) - end - end - - def reschedule_orphans_on!(hostname = nil) - redis.zrange(Manager.queue_key(hostname), 0, -1).each do |key| - klass = get_klass(key) - next unless klass - info = schedule_info(klass) - - if ['QUEUED', 'RUNNING'].include?(info.prev_result) && - (info.current_owner.blank? || !redis.get(info.current_owner)) - info.prev_result = 'ORPHAN' - info.next_run = Time.now.to_i - info.write! - end - end - end - - def get_klass(name) - name.constantize - rescue NameError - nil - end - - def tick - lock do - schedule_next_job - schedule_next_job(hostname) - end - end - - def schedule_next_job(hostname = nil) - (key, due), _ = redis.zrange Manager.queue_key(hostname), 0, 0, withscores: true - return unless key - - if due.to_i <= Time.now.to_i - klass = get_klass(key) - unless klass - # corrupt key, nuke it (renamed job or something) - redis.zrem Manager.queue_key(hostname), key - return - end - info = schedule_info(klass) - info.prev_run = Time.now.to_i - info.prev_result = "QUEUED" - info.prev_duration = -1 - info.next_run = nil - info.current_owner = identity_key - info.schedule! - @runner.enq(klass) - end - end - - def blocking_tick - tick - @runner.wait_till_done - end - - def stop! - @runner.stop! - self.class.current = nil - end - - def keep_alive_duration - 60 - end - - def keep_alive - redis.setex identity_key, keep_alive_duration, "" - end - - def lock - DistributedMutex.new(Manager.lock_key).synchronize do - yield - end - end - - def self.discover_schedules - # hack for developemnt reloader is crazytown - # multiple classes with same name can be in - # object space - unique = Set.new - schedules = [] - ObjectSpace.each_object(Scheduler::Schedule) do |schedule| - if schedule.scheduled? - next if unique.include?(schedule.to_s) - schedules << schedule - unique << schedule.to_s - end - end - schedules - end - - @mutex = Mutex.new - def self.seq - @mutex.synchronize do - @i ||= 0 - @i += 1 - end - end - - def identity_key - @identity_key ||= "_scheduler_#{hostname}:#{Process.pid}:#{self.class.seq}:#{SecureRandom.hex}" - end - - def self.lock_key - "_scheduler_lock_" - end - - def self.queue_key(hostname = nil) - if hostname - "_scheduler_queue_#{hostname}_" - else - "_scheduler_queue_" - end - end - - def self.schedule_key(klass, hostname = nil) - if hostname - "_scheduler_#{klass}_#{hostname}" - else - "_scheduler_#{klass}" - end - end - end -end diff --git a/lib/scheduler/schedule.rb b/lib/scheduler/schedule.rb deleted file mode 100644 index 05c8085bc1..0000000000 --- a/lib/scheduler/schedule.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Scheduler::Schedule - - def daily(options = nil) - if options - @daily = options - end - @daily - end - - def every(duration = nil) - if duration - @every = duration - if manager = Scheduler::Manager.current - manager.ensure_schedule!(self) - end - end - @every - end - - # schedule job indepndently on each host (looking at hostname) - def per_host - @per_host = true - end - - def is_per_host - @per_host - end - - def schedule_info - manager = Scheduler::Manager.without_runner - manager.schedule_info self - end - - def scheduled? - !!@every || !!@daily - end -end diff --git a/lib/scheduler/schedule_info.rb b/lib/scheduler/schedule_info.rb deleted file mode 100644 index d9348b2ea6..0000000000 --- a/lib/scheduler/schedule_info.rb +++ /dev/null @@ -1,138 +0,0 @@ -module Scheduler - class ScheduleInfo - attr_accessor :next_run, - :prev_run, - :prev_duration, - :prev_result, - :current_owner - - def initialize(klass, manager) - @klass = klass - @manager = manager - - data = nil - - if data = @manager.redis.get(key) - data = JSON.parse(data) - end - - if data - @next_run = data["next_run"] - @prev_run = data["prev_run"] - @prev_result = data["prev_result"] - @prev_duration = data["prev_duration"] - @current_owner = data["current_owner"] - end - rescue - # corrupt redis - @next_run = @prev_run = @prev_result = @prev_duration = @current_owner = nil - end - - # this means the schedule is going to fire, it is setup correctly - # invalid schedules are fixed by running "schedule!" - # this happens automatically after if fire by the manager - def valid? - return false unless @next_run - (!@prev_run && @next_run < Time.now.to_i + 5.minutes) || valid_every? || valid_daily? - end - - def valid_every? - return false unless @klass.every - !!@prev_run && - @prev_run <= Time.now.to_i && - @next_run < @prev_run + @klass.every * (1 + @manager.random_ratio) - end - - def valid_daily? - return false unless @klass.daily - return true if !@prev_run && @next_run && @next_run <= (Time.zone.now + 1.day).to_i - !!@prev_run && - @prev_run <= Time.zone.now.to_i && - @next_run < @prev_run + 1.day - end - - def schedule_every! - if !valid? && @prev_run - mixup = @klass.every * @manager.random_ratio - mixup = (mixup * Random.rand - mixup / 2).to_i - @next_run = @prev_run + mixup + @klass.every - end - - if !valid? - @next_run = Time.now.to_i + 5.minutes * Random.rand - end - end - - def schedule_daily! - return if valid? - - at = @klass.daily[:at] || 0 - today_begin = Time.zone.now.midnight.to_i - today_offset = DateTime.now.seconds_since_midnight - - # If it's later today - if at > today_offset - @next_run = today_begin + at - else - # Otherwise do it tomorrow - @next_run = today_begin + 1.day + at - end - end - - def schedule! - if @klass.every - schedule_every! - elsif @klass.daily - schedule_daily! - end - - write! - end - - def write! - - clear! - redis.set key, { - next_run: @next_run, - prev_run: @prev_run, - prev_duration: @prev_duration, - prev_result: @prev_result, - current_owner: @current_owner - }.to_json - - redis.zadd queue_key, @next_run, @klass if @next_run - end - - def del! - clear! - @next_run = @prev_run = @prev_result = @prev_duration = @current_owner = nil - end - - def key - if @klass.is_per_host - Manager.schedule_key(@klass, @manager.hostname) - else - Manager.schedule_key(@klass) - end - end - - def queue_key - if @klass.is_per_host - Manager.queue_key(@manager.hostname) - else - Manager.queue_key - end - end - - def redis - @manager.redis - end - - private - def clear! - redis.del key - redis.zrem queue_key, @klass - end - - end -end diff --git a/lib/scheduler/scheduler.rb b/lib/scheduler/scheduler.rb deleted file mode 100644 index e9f389194f..0000000000 --- a/lib/scheduler/scheduler.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Scheduler -end - -require_dependency 'scheduler/schedule' -require_dependency 'scheduler/schedule_info' -require_dependency 'scheduler/manager' -require_dependency 'scheduler/defer' diff --git a/lib/scheduler/views/history.erb b/lib/scheduler/views/history.erb deleted file mode 100644 index c1cc026ba7..0000000000 --- a/lib/scheduler/views/history.erb +++ /dev/null @@ -1,47 +0,0 @@ -
-
-

Scheduler History

-
-
- -
-
-
- <% if @scheduler_stats.length > 0 %> - - - - - - - - - - - - <% @scheduler_stats.each do |stat| %> - - - - - - - - - <% end %> - -
Job NameHostname:PidLive Slots deltaStarted AtDuration
<%= stat.name %><%= stat.hostname %>:<%= stat.pid %> - <% if stat.live_slots_start && stat.live_slots_finish %> - <%= stat.live_slots_finish - stat.live_slots_start %> - <% end %> - <%= sane_time stat.started_at %><%= sane_duration stat.duration_ms %> - <% if stat.success.nil? %> - RUNNING - <% elsif !stat.success %> - FAILED - <% end %> -
- <% end %> -
-
-
diff --git a/lib/scheduler/views/scheduler.erb b/lib/scheduler/views/scheduler.erb deleted file mode 100644 index 85fb6da8d9..0000000000 --- a/lib/scheduler/views/scheduler.erb +++ /dev/null @@ -1,73 +0,0 @@ -
- <% if Sidekiq.paused? %> -
-
-

SIDEKIQ IS PAUSED!

-
-
- <% end %> -
-

Recurring Jobs history

-
-
- -
-
- -
- <% if @schedules.length > 0 %> - - - - - - - - - - - <% @schedules.each do |schedule| %> - <% @info = schedule.schedule_info %> - - - - - - - - - <% end %> -
WorkerLast RunLast ResultLast DurationLast OwnerNext Run DueActions
- <%= schedule %> - - <% prev = @info.prev_run %> - <% if prev.nil? %> - Never - <% else %> - <%= relative_time(Time.at(prev)) %> - <% end %> - - <%= @info.prev_result %> - - <%= sane_duration @info.prev_duration %> - - <%= @info.current_owner %> - - <% next_run = @info.next_run %> - <% if next_run.nil? %> - Not Scheduled Yet - <% else %> - <%= relative_time(Time.at(next_run)) %> - <% end %> - -
" method="post"> - <%= csrf_tag if respond_to?(:csrf_tag) %> - -
-
- <% else %> -
No recurring jobs found.
- <% end %> -
-
-
diff --git a/lib/scheduler/web.rb b/lib/scheduler/web.rb deleted file mode 100644 index b2deac7bf6..0000000000 --- a/lib/scheduler/web.rb +++ /dev/null @@ -1,65 +0,0 @@ -# Based off sidetiq https://github.com/tobiassvn/sidetiq/blob/master/lib/sidetiq/web.rb -module Scheduler - module Web - VIEWS = File.expand_path('views', File.dirname(__FILE__)) unless defined? VIEWS - - def self.registered(app) - - app.helpers do - def sane_time(time) - return unless time - time - end - - def sane_duration(duration) - return unless duration - if duration < 1000 - "#{duration}ms" - elsif duration < 60 * 1000 - "#{'%.2f' % (duration / 1000.0)} secs" - end - end - end - - app.get "/scheduler" do - RailsMultisite::ConnectionManagement.with_connection("default") do - @manager = Scheduler::Manager.without_runner - @schedules = Scheduler::Manager.discover_schedules.sort do |a, b| - a_next = a.schedule_info.next_run - b_next = b.schedule_info.next_run - if a_next && b_next - a_next <=> b_next - elsif a_next - -1 - else - 1 - end - end - erb File.read(File.join(VIEWS, 'scheduler.erb')), locals: { view_path: VIEWS } - end - end - - app.get "/scheduler/history" do - @scheduler_stats = SchedulerStat.order('started_at desc').limit(200) - erb File.read(File.join(VIEWS, 'history.erb')), locals: { view_path: VIEWS } - end - - app.post "/scheduler/:name/trigger" do - halt 404 unless (name = params[:name]) - - RailsMultisite::ConnectionManagement.with_connection("default") do - klass = name.constantize - info = klass.schedule_info - info.next_run = Time.now.to_i - info.write! - - redirect "#{root_path}scheduler" - end - end - - end - end -end - -Sidekiq::Web.register(Scheduler::Web) -Sidekiq::Web.tabs["Scheduler"] = "scheduler" diff --git a/lib/tasks/scheduler.rake b/lib/tasks/scheduler.rake index 14e4018828..23b7cacf69 100644 --- a/lib/tasks/scheduler.rake +++ b/lib/tasks/scheduler.rake @@ -28,7 +28,7 @@ end desc "run every task the scheduler knows about in that order, use only for debugging" task 'scheduler:run_all' => :environment do - Scheduler::Manager.discover_schedules.each do |schedule| + MiniScheduler::Manager.discover_schedules.each do |schedule| puts "Running #{schedule}" time { schedule.new.execute({}) } end diff --git a/spec/components/scheduler/manager_spec.rb b/spec/components/scheduler/manager_spec.rb deleted file mode 100644 index aeaa9a3e64..0000000000 --- a/spec/components/scheduler/manager_spec.rb +++ /dev/null @@ -1,255 +0,0 @@ -# encoding: utf-8 -require 'rails_helper' -require 'scheduler/scheduler' - -describe Scheduler::Manager do - - module Testing - class RandomJob - extend ::Scheduler::Schedule - - def self.runs=(val) - @runs = val - end - - def self.runs - @runs ||= 0 - end - - every 5.minutes - - def perform - self.class.runs += 1 - sleep 0.001 - end - end - - class SuperLongJob - extend ::Scheduler::Schedule - - every 10.minutes - - def perform - sleep 1000 - end - end - - class PerHostJob - extend ::Scheduler::Schedule - - per_host - every 10.minutes - - def self.runs=(val) - @runs = val - end - - def self.runs - @runs ||= 0 - end - - def perform - self.class.runs += 1 - end - end - end - - let(:manager) { - Scheduler::Manager.new(DiscourseRedis.new, enable_stats: false) - } - - before do - expect(ActiveRecord::Base.connection_pool.connections.length).to eq(1) - @thread_count = Thread.list.count - - @backtraces = {} - Thread.list.each do |t| - @backtraces[t.object_id] = t.backtrace - end - end - - after do - manager.stop! - manager.remove(Testing::RandomJob) - manager.remove(Testing::SuperLongJob) - manager.remove(Testing::PerHostJob) - $redis.flushall - - # connections that are not in use must be removed - # otherwise active record gets super confused - ActiveRecord::Base.connection_pool.connections.reject { |c| c.in_use? }.each do |c| - ActiveRecord::Base.connection_pool.remove(c) - end - expect(ActiveRecord::Base.connection_pool.connections.length).to (be <= 1) - - on_thread_mismatch = lambda do - current = Thread.list.map { |t| t.object_id } - - old_threads = @backtraces.keys - extra = current - old_threads - - missing = old_threads - current - - if missing.length > 0 - STDERR.puts "\nMissing Threads #{missing.length} thread/s" - missing.each do |id| - STDERR.puts @backtraces[id] - STDERR.puts - end - end - - if extra.length > 0 - Thread.list.each do |thread| - if extra.include?(thread.object_id) - STDERR.puts "\nExtra Thread Backtrace:" - STDERR.puts thread.backtrace - STDERR.puts - end - end - end - end - - wait_for(on_fail: on_thread_mismatch) do - @thread_count == Thread.list.count - end - end - - it 'can disable stats' do - manager = Scheduler::Manager.new(DiscourseRedis.new, enable_stats: false) - expect(manager.enable_stats).to eq(false) - manager.stop! - - manager = Scheduler::Manager.new(DiscourseRedis.new) - expect(manager.enable_stats).to eq(true) - manager.stop! - end - - describe 'per host jobs' do - it "correctly schedules on multiple hosts" do - - freeze_time - - Testing::PerHostJob.runs = 0 - - hosts = ['a', 'b', 'c'] - - hosts.map do |host| - - manager = Scheduler::Manager.new(DiscourseRedis.new, hostname: host, enable_stats: false) - manager.ensure_schedule!(Testing::PerHostJob) - - info = manager.schedule_info(Testing::PerHostJob) - info.next_run = Time.now.to_i - 10 - info.write! - - manager - - end.each do |manager| - - manager.blocking_tick - manager.stop! - - end - - expect(Testing::PerHostJob.runs).to eq(3) - end - end - - describe '#sync' do - - it 'increases' do - expect(Scheduler::Manager.seq).to eq(Scheduler::Manager.seq - 1) - end - end - - describe '#tick' do - - it 'should nuke missing jobs' do - $redis.zadd Scheduler::Manager.queue_key, Time.now.to_i - 1000, "BLABLA" - manager.tick - expect($redis.zcard(Scheduler::Manager.queue_key)).to eq(0) - end - - it 'should recover from crashed manager' do - - info = manager.schedule_info(Testing::SuperLongJob) - info.next_run = Time.now.to_i - 1 - info.write! - - manager.tick - manager.stop! - - $redis.del manager.identity_key - - manager = Scheduler::Manager.new(DiscourseRedis.new, enable_stats: false) - manager.reschedule_orphans! - - info = manager.schedule_info(Testing::SuperLongJob) - expect(info.next_run).to be <= Time.now.to_i - - manager.stop! - end - - it 'should log when job finishes running' do - - Testing::RandomJob.runs = 0 - - info = manager.schedule_info(Testing::RandomJob) - info.next_run = Time.now.to_i - 1 - info.write! - - # with stats so we must be careful to cleanup - manager = Scheduler::Manager.new(DiscourseRedis.new) - manager.blocking_tick - manager.stop! - - stat = SchedulerStat.first - expect(stat).to be_present - expect(stat.duration_ms).to be > 0 - expect(stat.success).to be true - SchedulerStat.destroy_all - end - - it 'should only run pending job once' do - - Testing::RandomJob.runs = 0 - - info = manager.schedule_info(Testing::RandomJob) - info.next_run = Time.now.to_i - 1 - info.write! - - (0..5).map do - Thread.new do - manager = Scheduler::Manager.new(DiscourseRedis.new, enable_stats: false) - manager.blocking_tick - manager.stop! - end - end.map(&:join) - - expect(Testing::RandomJob.runs).to eq(1) - - info = manager.schedule_info(Testing::RandomJob) - expect(info.prev_run).to be <= Time.now.to_i - expect(info.prev_duration).to be > 0 - expect(info.prev_result).to eq("OK") - end - - end - - describe '#discover_schedules' do - it 'Discovers Testing::RandomJob' do - expect(Scheduler::Manager.discover_schedules).to include(Testing::RandomJob) - end - end - - describe '#next_run' do - it 'should be within the next 5 mins if it never ran' do - - manager.remove(Testing::RandomJob) - manager.ensure_schedule!(Testing::RandomJob) - - expect(manager.next_run(Testing::RandomJob)) - .to be_within(5.minutes.to_i).of(Time.now.to_i + 5.minutes) - end - end -end diff --git a/spec/components/scheduler/schedule_info_spec.rb b/spec/components/scheduler/schedule_info_spec.rb deleted file mode 100644 index 75fc3bc1bb..0000000000 --- a/spec/components/scheduler/schedule_info_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -# encoding: utf-8 -require 'rails_helper' -require 'scheduler/scheduler' - -describe Scheduler::ScheduleInfo do - - let(:manager) { Scheduler::Manager.new } - - context "every" do - class RandomJob - extend ::Scheduler::Schedule - - every 1.hour - - def perform - # work_it - end - end - - before do - @info = manager.schedule_info(RandomJob) - @info.del! - end - - after do - manager.stop! - $redis.del manager.class.queue_key - end - - it "is a scheduled job" do - expect(RandomJob).to be_scheduled - end - - it 'starts off invalid' do - expect(@info.valid?).to eq(false) - end - - it 'will have a due date in the next 5 minutes if it was blank' do - @info.schedule! - expect(@info.valid?).to eq(true) - expect(@info.next_run).to be_within(5.minutes).of(Time.now.to_i) - end - - it 'will have a due date within the next hour if it just ran' do - @info.prev_run = Time.now.to_i - @info.schedule! - expect(@info.valid?).to eq(true) - expect(@info.next_run).to be_within(1.hour * manager.random_ratio).of(Time.now.to_i + 1.hour) - end - - it 'is invalid if way in the future' do - @info.next_run = Time.now.to_i + 1.year - expect(@info.valid?).to eq(false) - end - end - - context "daily" do - - class DailyJob - extend ::Scheduler::Schedule - daily at: 11.hours - - def perform - end - end - - before do - freeze_time Time.parse("2010-01-10 10:00:00") - - @info = manager.schedule_info(DailyJob) - @info.del! - end - - after do - manager.stop! - $redis.del manager.class.queue_key - end - - it "is a scheduled job" do - expect(DailyJob).to be_scheduled - end - - it "starts off invalid" do - expect(@info.valid?).to eq(false) - end - - it "will have a due date at the appropriate time if blank" do - expect(@info.next_run).to eq(nil) - @info.schedule! - - expect(JSON.parse($redis.get(@info.key))["next_run"]) - .to eq((Time.zone.now.midnight + 11.hours).to_i) - - expect(@info.valid?).to eq(true) - end - - it 'is invalid if way in the future' do - @info.next_run = Time.now.to_i + 1.year - expect(@info.valid?).to eq(false) - end - end - -end From 6aee22b88f61184c0fdf03c7fc16c4cfea4ed5cd Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 1 Aug 2018 02:51:02 +0530 Subject: [PATCH 074/168] FIX: Onebox images are not downloaded locally without css class --- lib/cooked_post_processor.rb | 2 +- spec/components/cooked_post_processor_spec.rb | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index e89cee74bb..3f78317bca 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -193,7 +193,7 @@ class CookedPostProcessor end def oneboxed_images - @doc.css(".onebox-body img, .onebox img") + @doc.css(".onebox-body img, .onebox img, img.onebox") end def limit_size!(img) diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 0d1fa216a3..57fb16c590 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -513,6 +513,24 @@ describe CookedPostProcessor do expect(cpp).to be_dirty expect(cpp.html).to match_html "
GANGNAM STYLE
" end + + it "replaces downloaded onebox image" do + url = 'https://image.com/my-avatar' + image_url = 'https://image.com/avatar.png' + + Oneboxer.stubs(:onebox).with(url, anything).returns("") + + post = Fabricate(:post, raw: url) + upload = Fabricate(:upload, url: "https://test.s3.amazonaws.com/something.png") + + post.custom_fields[Post::DOWNLOADED_IMAGES] = { "//image.com/avatar.png": upload.id } + post.save_custom_fields + + cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true) + cpp.post_process_oneboxes + + expect(cpp.doc.to_s).to eq("

") + end end context ".post_process_oneboxes removes nofollow if add_rel_nofollow_to_user_content is disabled" do From 37252c1a5eb1316d8d3d28a9f4ece82d86d909c6 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 31 Jul 2018 17:35:13 -0400 Subject: [PATCH 075/168] UI: improves dashboard table reports - support for avatars - support for topic/post/user type in reports - improved totals row UI - minor css tweaks --- .../admin-report-table-header.js.es6 | 4 +- .../components/admin-report-table.js.es6 | 4 +- .../admin/components/admin-report.js.es6 | 34 +- .../javascripts/admin/models/report.js.es6 | 256 ++++++++++---- .../components/admin-report-table.hbs | 44 +-- .../common/admin/admin_report.scss | 4 +- .../common/admin/admin_report_table.scss | 20 +- app/models/admin_dashboard_next_data.rb | 2 +- .../admin_dashboard_next_general_data.rb | 2 +- app/models/admin_dashboard_next_index_data.rb | 2 +- app/models/report.rb | 317 +++++++++++++----- .../components/admin-report-test.js.es6 | 2 +- test/javascripts/models/report-test.js.es6 | 88 +++-- 13 files changed, 555 insertions(+), 224 deletions(-) diff --git a/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 index b7c569cc25..1f614db4a8 100644 --- a/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 @@ -3,10 +3,10 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ tagName: "th", classNames: ["admin-report-table-header"], - classNameBindings: ["label.property", "isCurrentSort"], + classNameBindings: ["label.mainProperty", "isCurrentSort"], attributeBindings: ["label.title:title"], - @computed("currentSortLabel.sort_property", "label.sort_property") + @computed("currentSortLabel.sortProperty", "label.sortProperty") isCurrentSort(currentSortField, labelSortField) { return currentSortField === labelSortField; }, diff --git a/app/assets/javascripts/admin/components/admin-report-table.js.es6 b/app/assets/javascripts/admin/components/admin-report-table.js.es6 index b39afc8a89..6d90eb73f8 100644 --- a/app/assets/javascripts/admin/components/admin-report-table.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table.js.es6 @@ -67,14 +67,14 @@ export default Ember.Component.extend({ const computedLabel = label.compute(row); const value = computedLabel.value; - if (computedLabel.type === "link" || (value && !isNumeric(value))) { + if (!computedLabel.countable || !value || !isNumeric(value)) { return undefined; } else { return sum + value; } }; - totalsRow[label.property] = rows.reduce(reducer, 0); + totalsRow[label.mainProperty] = rows.reduce(reducer, 0); }); return totalsRow; diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 index 74c96f814a..95d831af82 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -2,7 +2,7 @@ import Category from "discourse/models/category"; import { exportEntity } from "discourse/lib/export-csv"; import { outputExportResult } from "discourse/lib/export-result"; import { ajax } from "discourse/lib/ajax"; -import Report from "admin/models/report"; +import { SCHEMA_VERSION, default as Report } from "admin/models/report"; import computed from "ember-addons/ember-computed-decorators"; import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; @@ -189,24 +189,20 @@ export default Ember.Component.extend({ reportKey(dataSourceName, categoryId, groupId, startDate, endDate) { if (!dataSourceName || !startDate || !endDate) return null; - let reportKey = `reports:${dataSourceName}`; - - if (categoryId && categoryId !== "all") { - reportKey += `:${categoryId}`; - } else { - reportKey += `:`; - } - - reportKey += `:${startDate.replace(/-/g, "")}`; - reportKey += `:${endDate.replace(/-/g, "")}`; - - if (groupId && groupId !== "all") { - reportKey += `:${groupId}`; - } else { - reportKey += `:`; - } - - reportKey += `:`; + let reportKey = "reports:"; + reportKey += [ + dataSourceName, + categoryId, + startDate.replace(/-/g, ""), + endDate.replace(/-/g, ""), + groupId, + "[:prev_period]", + this.get("reportOptions.table.limit"), + SCHEMA_VERSION + ] + .filter(x => x) + .map(x => x.toString()) + .join(":"); return reportKey; }, diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 566aad0170..5f70b66e6d 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,87 +1,24 @@ import { escapeExpression } from "discourse/lib/utilities"; import { ajax } from "discourse/lib/ajax"; import round from "discourse/lib/round"; -import { fillMissingDates, isNumeric } from "discourse/lib/utilities"; +import { + fillMissingDates, + isNumeric, + formatUsername +} from "discourse/lib/utilities"; import computed from "ember-addons/ember-computed-decorators"; import { number, durationTiny } from "discourse/lib/formatter"; +import { renderAvatar } from "discourse/helpers/user-avatar"; + +// Change this line each time report format change +// and you want to ensure cache is reset +export const SCHEMA_VERSION = 1; const Report = Discourse.Model.extend({ average: false, percent: false, higher_is_better: true, - @computed("labels") - computedLabels(labels) { - return labels.map(label => { - const type = label.type; - const properties = label.properties; - const property = properties[0]; - - return { - title: label.title, - sort_property: label.sort_property || property, - property, - compute: row => { - let value = row[property]; - let escapedValue = escapeExpression(value); - let tooltip; - let base = { property, value, type }; - - if (value === null || typeof value === "undefined") { - return _.assign(base, { - value: null, - formatedValue: "-", - type: "undefined" - }); - } - - if (type === "seconds") { - return _.assign(base, { - formatedValue: escapeExpression(durationTiny(value)) - }); - } - - if (type === "link") { - return _.assign(base, { - formatedValue: `${escapedValue}` - }); - } - - if (type === "percent") { - return _.assign(base, { - formatedValue: `${escapedValue}%` - }); - } - - if (type === "number" || isNumeric(value)) - return _.assign(base, { - type: "number", - formatedValue: number(value) - }); - - if (type === "date") { - const date = moment(value, "YYYY-MM-DD"); - if (date.isValid()) { - return _.assign(base, { - formatedValue: date.format("LL") - }); - } - } - - if (type === "text") tooltip = escapedValue; - - return _.assign(base, { - tooltip, - type: type || "string", - formatedValue: escapedValue - }); - } - }; - }); - }, - @computed("modes") onlyTable(modes) { return modes.length === 1 && modes[0] === "table"; @@ -312,6 +249,179 @@ const Report = Discourse.Model.extend({ return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); }, + @computed("labels") + computedLabels(labels) { + return labels.map(label => { + const type = label.type; + + let mainProperty; + if (label.property) mainProperty = label.property; + else if (type === "user") mainProperty = label.properties["username"]; + else if (type === "topic") mainProperty = label.properties["title"]; + else if (type === "post") + mainProperty = label.properties["truncated_raw"]; + else mainProperty = label.properties[0]; + + return { + title: label.title, + sortProperty: label.sort_property || mainProperty, + mainProperty, + compute: row => { + const value = row[mainProperty]; + + if (type === "user") return this._userLabel(label.properties, row); + if (type === "post") return this._postLabel(label.properties, row); + if (type === "topic") return this._topicLabel(label.properties, row); + if (type === "seconds") + return this._secondsLabel(mainProperty, value); + if (type === "link") return this._linkLabel(label.properties, row); + if (type === "percent") + return this._percentLabel(mainProperty, value); + if (type === "number" || isNumeric(value)) { + return this._numberLabel(mainProperty, value); + } + if (type === "date") { + const date = moment(value, "YYYY-MM-DD"); + if (date.isValid()) + return this._dateLabel(mainProperty, value, date); + } + if (type === "text") return this._textLabel(mainProperty, value); + if (!value) return this._undefinedLabel(); + + return { + property: mainProperty, + value, + type: type || "string", + formatedValue: escapeExpression(value) + }; + } + }; + }); + }, + + _undefinedLabel() { + return { + value: null, + formatedValue: "-", + type: "undefined" + }; + }, + + _userLabel(properties, row) { + const username = row[properties.username]; + + if (!username) return this._undefinedLabel(); + + const user = Ember.Object.create({ + username, + name: formatUsername(username), + avatar_template: row[properties.avatar] + }); + + const avatarImg = renderAvatar(user, { + imageSize: "small", + ignoreTitle: true + }); + + const href = `/admin/users/${row[properties.id]}/${username}`; + + return { + type: "user", + property: properties.username, + value: username, + formatedValue: `${avatarImg}${username}` + }; + }, + + _topicLabel(properties, row) { + const topicTitle = row[properties.title]; + const topicId = row[properties.id]; + const href = `/t/-/${topicId}`; + + return { + type: "topic", + property: properties.title, + value: topicTitle, + formatedValue: `${topicTitle}` + }; + }, + + _postLabel(properties, row) { + const postTitle = row[properties.truncated_raw]; + const postNumber = row[properties.number]; + const topicId = row[properties.topic_id]; + const href = `/t/-/${topicId}/${postNumber}`; + + return { + type: "post", + property: properties.title, + value: postTitle, + formatedValue: `${postTitle}` + }; + }, + + _secondsLabel(property, value) { + return { + value, + property, + countable: true, + type: "seconds", + formatedValue: durationTiny(value) + }; + }, + + _percentLabel(property, value) { + return { + type: "percent", + property, + value, + formatedValue: `${value}%` + }; + }, + + _numberLabel(property, value) { + return { + type: "number", + countable: true, + property, + value, + formatedValue: number(value) + }; + }, + + _dateLabel(property, value, date) { + return { + type: "date", + property, + value, + formatedValue: date.format("LL") + }; + }, + + _textLabel(property, value) { + const escaped = escapeExpression(value); + + return { + type: "text", + property, + value, + formatedValue: escaped + }; + }, + + _linkLabel(properties, row) { + const property = properties[0]; + const value = row[property]; + return { + type: "link", + property, + value, + formatedValue: `${escapeExpression(value)}` + }; + }, + _computeChange(valAtT1, valAtT2) { return ((valAtT2 - valAtT1) / valAtT1) * 100; }, diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs index 330dc6f867..9a1af7d0ac 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs @@ -21,33 +21,35 @@ {{#each paginatedData as |data|}} {{admin-report-table-row data=data labels=model.computedLabels}} {{/each}} - - -{{#if showTotalForSample}} - {{i18n 'admin.dashboard.reports.totals_for_sample'}} - - - + {{#if showTotalForSample}} + + + + {{#each totalsForSample as |total|}} - + {{/each}} - -
+ {{i18n 'admin.dashboard.reports.totals_for_sample'}} +
{{total.formatedValue}} + {{total.formatedValue}} +
-{{/if}} + {{/if}} -{{#if showTotal}} - {{i18n 'admin.dashboard.reports.total'}} - - - - - + {{#if showTotal}} + + - -
-{{number model.total}}
+ {{i18n 'admin.dashboard.reports.total'}} +
-{{/if}} + + - + {{number model.total}} + + {{/if}} + +