From 105cf61ed93f61cba37e3673174d6b829040b6a1 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Wed, 20 Dec 2017 13:03:32 -0500 Subject: [PATCH 001/106] Implements https://meta.discourse.org/t/issue-user-changed-google-account-and-cant-connect-thru-his-profile/35028/18?u=supermathie --- config/locales/server.en.yml | 1 + lib/auth/google_oauth2_authenticator.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ada766f8ed..b462ca3ad8 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -139,6 +139,7 @@ en: max_username_length_range: "You cannot set the maximum below the minimum." default_categories_already_selected: "You cannot select a category used in another list." s3_upload_bucket_is_required: "You cannot enable uploads to S3 unless you've provided the 's3_upload_bucket'." + conflicting_google_user_id: 'The Google Account ID for this account has changed, for protection this requires manual intervention. Please contact the site administrator with the following reference:
https://meta.discourse.org/t/76575' activemodel: errors: diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index 310f2ab9e9..dcee38d217 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -21,6 +21,19 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator if !result.user && !result.email.blank? && result.email_valid result.user = User.find_by_email(result.email) if result.user + # we've matched an existing user to this login attempt... + if result.user.google_user_info && result.user.google_user_info.google_user_id != google_hash[:google_user_id] + # but the user has changed the google account used to log in... + if result.user.google_user_info.email != google_hash[:email] + # the user changed their email, go ahead and scrub the old record + result.user.google_user_info.destroy! + else + # same email address but different account? likely a takeover scenario + result.failed = true + result.failed_reason = I18n.t('errors.conflicting_google_user_id') + return result + end + end ::GoogleUserInfo.create({ user_id: result.user.id }.merge(google_hash)) end end From e756d020881b3a60122bd76ac740e5aaae028c21 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 11:23:45 +1100 Subject: [PATCH 002/106] FIX: create topic keyboard shortcut not checking permissions --- .../javascripts/discourse/lib/keyboard-shortcuts.js.es6 | 4 +++- app/serializers/current_user_serializer.rb | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index b011f8bab0..c3a7ecd27c 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -169,7 +169,9 @@ export default { }, createTopic() { - this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC}); + if (this.currentUser && this.currentUser.can_create_topic) { + this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC}); + } }, pinUnpinTopic() { diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index c91ebef82c..46fa8fe05c 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -38,7 +38,12 @@ class CurrentUserSerializer < BasicUserSerializer :previous_visit_at, :seen_notification_id, :primary_group_id, - :primary_group_name + :primary_group_name, + :can_create_topic + + def can_create_topic + scope.can_create_topic?(nil) + end def include_site_flagged_posts_count? object.staff? From 6bf3bee473f2008ee756256a1ce11a669ef1a1a0 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 12:30:41 +1100 Subject: [PATCH 003/106] FIX: when leaving PM tab revert to user scoped search --- .../discourse/routes/build-private-messages-route.js.es6 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 index 63fa6d56e8..b20e9e1b3d 100644 --- a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 @@ -32,7 +32,10 @@ export default (viewName, path) => { }, deactivate() { - this.searchService.set('contextType', 'private_messages'); + this.searchService.set( + 'searchContext', + this.controllerFor("user").get("model.searchContext") + ); } }); }; From aabac55edd718eb2b70c2a5a92a4c8636fbc47d1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 21 Dec 2017 09:47:32 +0800 Subject: [PATCH 004/106] Better ENV name for QUnit's seed. --- lib/tasks/qunit.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index 13ff929dec..3c52e5430a 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -47,7 +47,7 @@ task "qunit:test", [:timeout, :qunit_path] => :environment do |_, args| test_path = "#{Rails.root}/vendor/assets/javascripts" qunit_path = args[:qunit_path] || "/qunit" cmd = "node #{test_path}/run-qunit.js http://localhost:#{port}#{qunit_path}" - options = { seed: (ENV["SEED"] || Random.new.seed) } + options = { seed: (ENV["QUNIT_SEED"] || Random.new.seed) } %w{module filter qunit_skip_core qunit_single_plugin}.each do |arg| options[arg] = ENV[arg.upcase] if ENV[arg.upcase].present? From 2423b18839481c9dabf7b34b18146f79b68e3abf Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 13:00:08 +1100 Subject: [PATCH 005/106] FIX: links inside quotes not opening in new tab --- .../discourse/lib/click-track.js.es6 | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 648b49299b..6247973d45 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -26,7 +26,7 @@ export default { } // don't track links in quotes or in elided part - if ($link.parents('aside.quote,.elided').length) { return true; } + let tracking = $link.parents('aside.quote,.elided').length === 0; let href = $link.attr('href') || $link.data('href'); @@ -39,26 +39,31 @@ export default { const userId = $link.data('user-id') || $article.data('user-id'); const ownLink = userId && (userId === Discourse.User.currentProp('id')); - let trackingUrl = Discourse.getURL('/clicks/track?url=' + encodeURIComponent(href)); + let destUrl = href; - if (postId && !$link.data('ignore-post-id')) { - trackingUrl += "&post_id=" + encodeURI(postId); - } - if (topicId) { - trackingUrl += "&topic_id=" + encodeURI(topicId); - } + if (tracking) { - // Update badge clicks unless it's our own - if (!ownLink) { - const $badge = $('span.badge', $link); - if ($badge.length === 1) { - // don't update counts in category badge nor in oneboxes (except when we force it) - if (isValidLink($link)) { - const html = $badge.html(); - const key = `${new Date().toLocaleDateString()}-${postId}-${href}`; - if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) { - sessionStorage.setItem(key, true); - $badge.html(parseInt(html, 10) + 1); + destUrl = Discourse.getURL('/clicks/track?url=' + encodeURIComponent(href)); + + if (postId && !$link.data('ignore-post-id')) { + destUrl += "&post_id=" + encodeURI(postId); + } + if (topicId) { + destUrl += "&topic_id=" + encodeURI(topicId); + } + + // Update badge clicks unless it's our own + if (!ownLink) { + const $badge = $('span.badge', $link); + if ($badge.length === 1) { + // don't update counts in category badge nor in oneboxes (except when we force it) + if (isValidLink($link)) { + const html = $badge.html(); + const key = `${new Date().toLocaleDateString()}-${postId}-${href}`; + if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) { + sessionStorage.setItem(key, true); + $badge.html(parseInt(html, 10) + 1); + } } } } @@ -66,12 +71,12 @@ export default { // If they right clicked, change the destination href if (e.which === 3) { - $link.attr('href', Discourse.SiteSettings.track_external_right_clicks ? trackingUrl : href); + $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 (wantsNewWindow(e)) { + if (tracking && wantsNewWindow(e)) { ajax("/clicks/track", { data: { url: href, @@ -109,7 +114,7 @@ export default { } // If we're on the same site, use the router and track via AJAX - if (DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { + if (tracking && DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { ajax("/clicks/track", { data: { url: href, @@ -125,9 +130,9 @@ export default { // Otherwise, use a custom URL with a redirect if (Discourse.User.currentProp('external_links_in_new_tab')) { - window.open(trackingUrl, '_blank').focus(); + window.open(destUrl, '_blank').focus(); } else { - DiscourseURL.redirectTo(trackingUrl); + DiscourseURL.redirectTo(destUrl); } return false; From 252cbd8635ab6b5dd66be856ba4a2c1f8f862dd1 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 13:09:18 +1100 Subject: [PATCH 006/106] FIX: keyboard shortcut allowed you to open composer on closed topics --- app/assets/javascripts/discourse/controllers/topic.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index a2a8392ba5..80209d7b73 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -272,7 +272,7 @@ export default Ember.Controller.extend(BufferedContent, { const quoteState = this.get('quoteState'); const postStream = this.get('model.postStream'); - if (!postStream) return; + if (!postStream || !topic || !topic.get('details.can_create_post')) { return; } const quotedPost = postStream.findLoadedPost(quoteState.postId); const quotedText = Quote.build(quotedPost, quoteState.buffer); From 7b0f3f4e34a422ef47b7ad0494cf5a1bafb944fb Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 20 Dec 2017 21:24:59 -0500 Subject: [PATCH 007/106] FIX: only staff highlight main post, not embedded replies --- app/assets/stylesheets/common/base/topic-post.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index e964fd720f..de4ed21e79 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -67,7 +67,7 @@ // add staff color .moderator { - .cooked { + .regular > .cooked { background-color: dark-light-choose($highlight-low, $highlight-medium); padding: 10px; img:not(.thumbnail) { @@ -75,7 +75,7 @@ height: auto; } } - .names { + .clearfix > .topic-meta-data > .names { span.user-title { background-color: dark-light-choose($highlight-low, $highlight-medium); color: dark-light-choose($primary-high, $secondary-low); From 8f6e2b718666821e272cf5b1e82b01cfc656b418 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 21 Dec 2017 10:32:14 +0800 Subject: [PATCH 008/106] Fix unhandled promise in smoke test. --- test/smoke_test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/smoke_test.js b/test/smoke_test.js index b4c3af8c89..4c1e151a0c 100644 --- a/test/smoke_test.js +++ b/test/smoke_test.js @@ -53,7 +53,9 @@ const path = require('path'); page.on('console', msg => console.log(`PAGE LOG: ${msg.text}`)); - await page.goto(url); + await exec("go to site", () => { + return page.goto(url); + }); await exec("expect a log in button in the header", () => { return page.waitForSelector("header .login-button", { visible: true }); From 081959227de72c7473d36618bb02ddea3e42ebcd Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 15:20:30 +1100 Subject: [PATCH 009/106] FIX: unicode titles missing when visiting topic from topic list --- .../javascripts/discourse/routes/topic.js.es6 | 2 +- app/serializers/listable_topic_serializer.rb | 11 ++++++++++- app/serializers/topic_view_serializer.rb | 2 +- lib/freedom_patches/match.rb | 8 ++++++++ lib/freedom_patches/scrub.rb | 16 ---------------- 5 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 lib/freedom_patches/match.rb delete mode 100644 lib/freedom_patches/scrub.rb diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 5086722534..83205ff599 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -20,7 +20,7 @@ const TopicRoute = Discourse.Route.extend({ titleToken() { const model = this.modelFor('topic'); if (model) { - const result = model.get('unicode_title') ? model.get('unicode_title') : model.get('title'), + const result = model.get('unicode_title') || model.get('title'), cat = model.get('category'); // Only display uncategorized in the title tag if it was renamed diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb index 13a87235c1..aeecddbeae 100644 --- a/app/serializers/listable_topic_serializer.rb +++ b/app/serializers/listable_topic_serializer.rb @@ -22,10 +22,19 @@ class ListableTopicSerializer < BasicTopicSerializer :is_warning, :notification_level, :bookmarked, - :liked + :liked, + :unicode_title has_one :last_poster, serializer: BasicUserSerializer, embed: :objects + def include_unicode_title? + object.title.match?(/:[\w\-+]+:/) + end + + def unicode_title + Emoji.gsub_emoji_to_unicode(object.title) + end + def highest_post_number (scope.is_staff? && object.highest_staff_post_number) || object.highest_post_number end diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 9f6ea937e8..9297793726 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -267,7 +267,7 @@ class TopicViewSerializer < ApplicationSerializer end def include_unicode_title? - !!(object.topic.title =~ /:([\w\-+]*):/) + object.topic.title.match?(/:[\w\-+]+:/) end def unicode_title diff --git a/lib/freedom_patches/match.rb b/lib/freedom_patches/match.rb new file mode 100644 index 0000000000..56084edaf6 --- /dev/null +++ b/lib/freedom_patches/match.rb @@ -0,0 +1,8 @@ +class String + # new to Ruby 2.4, fastest way of matching a string to a regex + unless method_defined? :match? + def match?(regex) + !!(self =~ regex) + end + end +end diff --git a/lib/freedom_patches/scrub.rb b/lib/freedom_patches/scrub.rb deleted file mode 100644 index 05c1dba04d..0000000000 --- a/lib/freedom_patches/scrub.rb +++ /dev/null @@ -1,16 +0,0 @@ -class String - # A poor man's scrub, Ruby 2.1 has a much better implementation, but this will do - unless method_defined? :scrub - def scrub(replace_char = nil) - str = dup.force_encoding("utf-8") - - unless str.valid_encoding? - # work around bust string with a double conversion - str.encode!("utf-16", "utf-8", invalid: :replace) - str.encode!("utf-8", "utf-16") - end - - str - end - end -end From 62a27f9d5768ff998d96fa3f4cbc1b365c1bc539 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 16:13:57 +1100 Subject: [PATCH 010/106] FEATURE: warn if attempting to mention a group with too many members --- .../components/composer-editor.js.es6 | 2 +- .../discourse/controllers/composer.js.es6 | 20 ++++++++++++++----- .../discourse/lib/link-mentions.js.es6 | 5 ++++- app/controllers/users_controller.rb | 15 ++++++++++++-- config/locales/client.en.yml | 1 + 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 6d56b6b915..9f06d80b7f 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -363,7 +363,7 @@ export default Ember.Component.extend({ const $e = $(e); var name = $e.data('name'); if (found.indexOf(name) === -1){ - this.sendAction('groupsMentioned', [{name: name, user_count: $e.data('mentionable-user-count')}]); + this.sendAction('groupsMentioned', [{name: name, user_count: $e.data('mentionable-user-count'), max_mentions: $e.data('max-mentions')}]); found.push(name); } }); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 20b8736c6f..0e6f9c6193 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -378,11 +378,21 @@ export default Ember.Controller.extend({ groupsMentioned(groups) { if (!this.get('model.creatingPrivateMessage') && !this.get('model.topic.isPrivateMessage')) { groups.forEach(group => { - const body = I18n.t('composer.group_mentioned', { - group: "@" + group.name, - count: group.user_count, - group_link: Discourse.getURL(`/groups/${group.name}/members`) - }); + let body; + + if (group.max_mentions < group.user_count) { + body = I18n.t('composer.group_mentioned_limit', { + group: "@" + group.name, + max: group.max_mentions, + group_link: Discourse.getURL(`/groups/${group.name}/members`) + }); + } else { + body = I18n.t('composer.group_mentioned', { + group: "@" + group.name, + count: group.user_count, + group_link: Discourse.getURL(`/groups/${group.name}/members`) + }); + } this.appEvents.trigger('composer-messages:create', { extraClass: 'custom-body', diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 index 688207c5b9..8aa802f80f 100644 --- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 @@ -2,13 +2,15 @@ import { ajax } from 'discourse/lib/ajax'; import { userPath } from 'discourse/lib/url'; import { formatUsername } from 'discourse/lib/utilities'; +let maxGroupMention; + function replaceSpan($e, username, opts) { let extra = ""; let extraClass = ""; if (opts && opts.group) { if (opts.mentionable) { - extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}'`; + extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}' data-max-mentions='${maxGroupMention}'`; extraClass = "notify"; } $e.replaceWith(`@${username}`); @@ -61,6 +63,7 @@ export function fetchUnseenMentions(usernames, topic_id) { r.valid_groups.forEach(vg => foundGroups[vg] = true); r.mentionable_groups.forEach(mg => mentionableGroups[mg.name] = mg); r.cannot_see.forEach(cs => cannotSee[cs] = true); + maxGroupMention = r.max_users_notified_per_group_mention; usernames.forEach(u => checked[u] = true); return r; }); diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3ce9e029bc..c81c28dc84 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -248,7 +248,12 @@ class UsersController < ApplicationController Group.mentionable(current_user) .where(name: usernames) .pluck(:name, :user_count) - .map { |name, user_count| { name: name, user_count: user_count } } + .map do |name, user_count| + { + name: name, + user_count: user_count + } + end end usernames -= groups @@ -267,7 +272,13 @@ class UsersController < ApplicationController .where(username_lower: usernames) .pluck(:username_lower) - render json: { valid: result, valid_groups: groups, mentionable_groups: mentionable_groups, cannot_see: cannot_see } + render json: { + valid: result, + valid_groups: groups, + mentionable_groups: mentionable_groups, + cannot_see: cannot_see, + max_users_notified_per_group_mention: SiteSetting.max_users_notified_per_group_mention + } end def render_available_true diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a7fc27bf47..eed1dd915d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1211,6 +1211,7 @@ en: similar_topics: "Your topic is similar to..." drafts_offline: "drafts offline" + group_mentioned_limit: "Warning! you mentioned {{group}}, however it has more members than the administrator configured limit of {{max}} users. Nobody will be notified. " group_mentioned: one: "By mentioning {{group}}, you are about to notify 1 person – are you sure?" other: "By mentioning {{group}}, you are about to notify {{count}} people – are you sure?" From b3c086a6366abe4cce93bf7c64efaaa2610f2f98 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 16:15:07 +1100 Subject: [PATCH 011/106] copy --- 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 eed1dd915d..42277a4ecd 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1211,7 +1211,7 @@ en: similar_topics: "Your topic is similar to..." drafts_offline: "drafts offline" - group_mentioned_limit: "Warning! you mentioned {{group}}, however it has more members than the administrator configured limit of {{max}} users. Nobody will be notified. " + group_mentioned_limit: "Warning! You mentioned {{group}}, however it has more members than the administrator configured limit of {{max}} users. Nobody will be notified. " group_mentioned: one: "By mentioning {{group}}, you are about to notify 1 person – are you sure?" other: "By mentioning {{group}}, you are about to notify {{count}} people – are you sure?" From ba3bf9c7bb2fcc83a57a5d74737a885f3243501f Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 20 Dec 2017 21:32:54 -0800 Subject: [PATCH 012/106] minor copyedit --- 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 42277a4ecd..65ba34cbd6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1211,7 +1211,7 @@ en: similar_topics: "Your topic is similar to..." drafts_offline: "drafts offline" - group_mentioned_limit: "Warning! You mentioned {{group}}, however it has more members than the administrator configured limit of {{max}} users. Nobody will be notified. " + group_mentioned_limit: "Warning! You mentioned {{group}}, however this group has more members than the administrator configured mention limit of {{max}} users. Nobody will be notified. " group_mentioned: one: "By mentioning {{group}}, you are about to notify 1 person – are you sure?" other: "By mentioning {{group}}, you are about to notify {{count}} people – are you sure?" From 6ecf37c482603cba27510e41723fdd1909d0a135 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 21 Dec 2017 12:27:17 +0800 Subject: [PATCH 013/106] Improve URL validation to check for a valid host. Parsing a URL with `URI` is not sufficient as the following cases are considered valid: URI.parse("http://https://google.com") => # --- app/models/user_profile.rb | 7 +--- lib/validators/url_validator.rb | 10 ++++-- .../validators/url_validator_spec.rb | 33 +++++++++++++++++++ spec/models/user_profile_spec.rb | 17 ++++++---- 4 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 spec/components/validators/url_validator_spec.rb diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index 1d07636a50..d10b63f034 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -1,13 +1,8 @@ class UserProfile < ActiveRecord::Base belongs_to :user, inverse_of: :user_profile - # This is not very picky about most DNS labels (the bits between the - # periods), but isn't taking much guff from the TLD. No leading - # digit, and no hyphens unless IDN. - WEBSITE_REGEXP = /(^$)|(^(https?:\/\/)?([a-z0-9][a-z0-9-]*\.)+([a-z][a-z0-9]+|xn--[a-z0-9-]+)(\/.*)?$)/i - validates :bio_raw, length: { maximum: 3000 } - validates :website, format: { with: WEBSITE_REGEXP }, allow_blank: true, if: Proc.new { |c| c.new_record? || c.website_changed? } + validates :website, url: true, allow_blank: true, if: Proc.new { |c| c.new_record? || c.website_changed? } validates :user, presence: true before_save :cook after_save :trigger_badges diff --git a/lib/validators/url_validator.rb b/lib/validators/url_validator.rb index c3295e6368..7c91bf206b 100644 --- a/lib/validators/url_validator.rb +++ b/lib/validators/url_validator.rb @@ -1,9 +1,15 @@ class UrlValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) if value.present? - uri = URI.parse(value) rescue nil + valid = + begin + uri = URI.parse(value) + uri.is_a?(URI::HTTP) && !uri.host.nil? && uri.host.include?(".") + rescue + nil + end - unless uri + unless valid record.errors[attribute] << (options[:message] || I18n.t('errors.messages.invalid')) end end diff --git a/spec/components/validators/url_validator_spec.rb b/spec/components/validators/url_validator_spec.rb new file mode 100644 index 0000000000..5554700b77 --- /dev/null +++ b/spec/components/validators/url_validator_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' +require 'validators/topic_title_length_validator' + +RSpec.describe UrlValidator do + let(:record) { Fabricate.build(:user_profile, user: Fabricate.build(:user)) } + let(:validator) { described_class.new(attributes: :website) } + subject(:validate) { validator.validate_each(record, :website, record.website) } + + [ + "http://https://google.com", + "http://google/", + "ftp://ftp.google.com", + "http:///what.is.this", + 'http://meta.discourse.org TEST' + ].each do |invalid_url| + it "#{invalid_url} should not be valid" do + record.website = invalid_url + validate + expect(record.errors[:website]).to be_present + end + end + + [ + "http://discourse.productions", + "https://google.com", + ].each do |valid_url| + it "#{valid_url} should be valid" do + record.website = valid_url + validate + expect(record.errors[:website]).to_not be_present + end + end +end diff --git a/spec/models/user_profile_spec.rb b/spec/models/user_profile_spec.rb index 7ae25e14cd..0fffc455aa 100644 --- a/spec/models/user_profile_spec.rb +++ b/spec/models/user_profile_spec.rb @@ -55,18 +55,21 @@ describe UserProfile do end context "website validation" do - let(:user) { Fabricate(:user) } + let(:user_profile) { Fabricate.build(:user_profile, user: Fabricate(:user)) } - it "ensures website is valid" do - expect(Fabricate.build(:user_profile, user: user, website: "http://https://google.com")).not_to be_valid - expect(Fabricate.build(:user_profile, user: user, website: "http://discourse.productions")).to be_valid - expect(Fabricate.build(:user_profile, user: user, website: "https://google.com")).to be_valid + it "should not allow invalid URLs" do + user_profile.website = "http://https://google.com" + expect(user_profile).to_not be_valid end it "validates website domain if user_website_domains_whitelist setting is present" do SiteSetting.user_website_domains_whitelist = "discourse.org" - expect(Fabricate.build(:user_profile, user: user, website: "https://google.com")).not_to be_valid - expect(Fabricate.build(:user_profile, user: user, website: "http://discourse.org")).to be_valid + + user_profile.website = "https://google.com" + expect(user_profile).not_to be_valid + + user_profile.website = "http://discourse.org" + expect(user_profile).to be_valid end end From 4b51871f6a906364ac34be13d0bc5edf6cbbf9c7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 21 Dec 2017 14:22:55 +0800 Subject: [PATCH 014/106] Treat non-ascii URLs in `UrlValidator`. --- lib/validators/url_validator.rb | 7 ++++++- spec/components/validators/url_validator_spec.rb | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/validators/url_validator.rb b/lib/validators/url_validator.rb index 7c91bf206b..7edfe431f3 100644 --- a/lib/validators/url_validator.rb +++ b/lib/validators/url_validator.rb @@ -5,7 +5,12 @@ class UrlValidator < ActiveModel::EachValidator begin uri = URI.parse(value) uri.is_a?(URI::HTTP) && !uri.host.nil? && uri.host.include?(".") - rescue + rescue URI::InvalidURIError => e + if (e.message =~ /URI must be ascii only/) + value = URI.encode(value) + retry + end + nil end diff --git a/spec/components/validators/url_validator_spec.rb b/spec/components/validators/url_validator_spec.rb index 5554700b77..4278fc2ecb 100644 --- a/spec/components/validators/url_validator_spec.rb +++ b/spec/components/validators/url_validator_spec.rb @@ -23,6 +23,8 @@ RSpec.describe UrlValidator do [ "http://discourse.productions", "https://google.com", + 'http://xn--nw2a.xn--j6w193g/', + "http://見.香港/", ].each do |valid_url| it "#{valid_url} should be valid" do record.website = valid_url From 129d924c0d2dce5be71fe6c3ce93431bf32b6a7c Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Thu, 21 Dec 2017 12:58:57 +0530 Subject: [PATCH 015/106] Dont support single row or column tables in HTML to Markdown conversion --- .../discourse/lib/to-markdown.js.es6 | 17 ++++++++++++++--- test/javascripts/lib/to-markdown-test.js.es6 | 14 +++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 index 8f81536f22..1d2f2cbe25 100644 --- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 +++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 @@ -234,9 +234,20 @@ class Tag { static table() { return class extends Tag.block("table") { decorate(text) { - text = super.decorate(text); - const splitterRow = text.split("|\n")[0].match(/\|/g).map(() => "| --- ").join("") + "|\n"; - text = text.replace("|\n", "|\n" + splitterRow).replace(/\|\n{2,}\|/g, "|\n|"); + text = super.decorate(text).replace(/\|\n{2,}\|/g, "|\n|"); + const rows = text.trim().split("\n"); + const pipes = rows[0].match(/\|/g); + const isValid = rows.length > 1 && + pipes.length > 2 && + rows.reduce((a, c) => a && c.match(/\|/g).length <= pipes.length); + + if (!isValid) { + throw "Unsupported table format for Markdown conversion"; + } + + const splitterRow = pipes.slice(1).map(() => "| --- ").join("") + "|\n"; + text = text.replace("|\n", "|\n" + splitterRow); + return text; } }; diff --git a/test/javascripts/lib/to-markdown-test.js.es6 b/test/javascripts/lib/to-markdown-test.js.es6 index 4ebb94ec9a..0a33d642b9 100644 --- a/test/javascripts/lib/to-markdown-test.js.es6 +++ b/test/javascripts/lib/to-markdown-test.js.es6 @@ -109,7 +109,7 @@ QUnit.test("converts table tags", assert => { }); QUnit.test("returns empty string if table format not supported", assert => { - const html = ` + let html = `
@@ -117,6 +117,18 @@ QUnit.test("returns empty string if table format not supported", assert => {
Headi\n\nng 1Head 2
Loremipsum
`; assert.equal(toMarkdown(html), ""); + + html = ` + + + + +
Heading 1
Lorem
sit amet
+ `; + assert.equal(toMarkdown(html), ""); + + html = `
Loremsit amet
`; + assert.equal(toMarkdown(html), ""); }); QUnit.test("converts img tag", assert => { From e5cc0f1358742efc4e350a7a27386ddb4eb9d9b7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 21 Dec 2017 15:46:57 +0800 Subject: [PATCH 016/106] Test `discourse-patreon` on Travis as well. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e4f402f801..b44d412167 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,6 +45,7 @@ before_install: - git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies - git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration - git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign + - git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon - export PATH=$HOME/.yarn/bin:$PATH install: From 66aa8691eda5b5efbd4922175fcd9ff56dbf9173 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 21 Dec 2017 16:03:43 +0800 Subject: [PATCH 017/106] Fix JS travis tests incorrectly passing. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b44d412167..7eb40ed6b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,7 @@ script: bundle exec rake db:create db:migrate if [ '$QUNIT_RUN' == '1' ]; then - bundle exec rake qunit:test['400000'] + bundle exec rake qunit:test['400000'] && \ bundle exec rake plugin:spec else bundle exec rspec && bundle exec rake plugin:spec From ab2215e62978af049539a97b5429857938c3926e Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 21 Dec 2017 20:18:59 +1100 Subject: [PATCH 018/106] fix broken test --- test/javascripts/lib/click-track-test.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index 8bfb89e888..dc6013ee08 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -70,7 +70,8 @@ QUnit.test("does not track clicks on back buttons", function(assert) { }); QUnit.test("does not track clicks in quotes", function(assert) { - assert.ok(track(generateClickEventOn('.inside-quote'))); + track(generateClickEventOn('.inside-quote')); + assert.ok(DiscourseURL.redirectTo.calledWith("http://discuss.domain.com")); }); QUnit.test("does not track clicks on category badges", assert => { From 16076f9ab8e5979b19f0b4a446741ac540ff7d56 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 21 Dec 2017 12:06:26 +0530 Subject: [PATCH 019/106] bump onebox version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index f242cd5692..450cd0a4fc 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.30' +gem 'onebox', '1.8.31' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index a763d7c89d..0e0cc65fc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -232,7 +232,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.30) + onebox (1.8.31) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -469,7 +469,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.30) + onebox (= 1.8.31) openid-redis-store pg pry-nav From 727a45185d40b6c2288877b0a9a42c62002fcf51 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 21 Dec 2017 11:26:38 +0100 Subject: [PATCH 020/106] FIX: regex should behave the same in Ruby and Postgres --- app/models/post.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/post.rb b/app/models/post.rb index 800221ed1a..a218d49d79 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -103,7 +103,7 @@ class Post < ActiveRecord::Base when 'string' where('raw ILIKE ?', "%#{pattern}%") when 'regex' - where('raw ~ ?', pattern) + where('raw ~ ?', "(?n)#{pattern}") end } From 7b58afe677446caee5c10ab784baaff17ab692e6 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 21 Dec 2017 14:45:59 +0100 Subject: [PATCH 021/106] FIX: ProcessPost job failed for posts that have no user --- app/jobs/regular/process_post.rb | 2 +- spec/jobs/process_post_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/jobs/regular/process_post.rb b/app/jobs/regular/process_post.rb index 8a9cba11bb..88e340501a 100644 --- a/app/jobs/regular/process_post.rb +++ b/app/jobs/regular/process_post.rb @@ -38,7 +38,7 @@ module Jobs end end - if !post.user.staff? && !post.user.staged + if !post.user&.staff? && !post.user&.staged? s = post.cooked s << " #{post.topic.title}" if post.post_number == 1 if !args[:bypass_bump] && WordWatcher.new(s).should_flag? diff --git a/spec/jobs/process_post_spec.rb b/spec/jobs/process_post_spec.rb index 9c21d4f5fb..137eb176db 100644 --- a/spec/jobs/process_post_spec.rb +++ b/spec/jobs/process_post_spec.rb @@ -62,6 +62,19 @@ describe Jobs::ProcessPost do expect { Jobs::ProcessPost.new.execute(post_id: post.id) }.to change { TopicLink.count }.by(2) end + it "works for posts that belong to no existing user" do + cooked = post.cooked + + post.update_columns(cooked: "frogs", user_id: nil) + Jobs::ProcessPost.new.execute(post_id: post.id, cook: true) + post.reload + expect(post.cooked).to eq(cooked) + + post.update_columns(cooked: "frogs", user_id: User.maximum("id") + 1) + Jobs::ProcessPost.new.execute(post_id: post.id, cook: true) + post.reload + expect(post.cooked).to eq(cooked) + end end end From dea3e84f185fb856448369f4e39b7b33ffcd042b Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 21 Dec 2017 12:59:19 -0500 Subject: [PATCH 022/106] UX: Reducing number of font-sizes used in common --- .../stylesheets/common/base/_topic-list.scss | 12 ++++++------ .../stylesheets/common/base/category-list.scss | 6 +++--- app/assets/stylesheets/common/base/compose.scss | 4 ++-- .../stylesheets/common/base/directory.scss | 2 +- .../stylesheets/common/base/discourse.scss | 10 +++++----- .../base/edit-topic-status-update-modal.scss | 2 +- .../stylesheets/common/base/exception.scss | 4 ++-- app/assets/stylesheets/common/base/group.scss | 10 ++++++---- app/assets/stylesheets/common/base/groups.scss | 4 ++-- app/assets/stylesheets/common/base/login.scss | 6 +++--- .../stylesheets/common/base/magnific-popup.scss | 1 - .../stylesheets/common/base/menu-panel.scss | 6 +++--- app/assets/stylesheets/common/base/modal.scss | 4 ++-- .../stylesheets/common/base/not-found.scss | 2 +- app/assets/stylesheets/common/base/onebox.scss | 4 ++-- .../stylesheets/common/base/request_access.scss | 4 ++-- app/assets/stylesheets/common/base/search.scss | 4 ++-- .../stylesheets/common/base/share_link.scss | 6 +++--- app/assets/stylesheets/common/base/tagging.scss | 10 +++++----- .../stylesheets/common/base/topic-post.scss | 6 +++--- .../stylesheets/common/base/user-badges.scss | 10 +++++----- app/assets/stylesheets/common/base/user.scss | 17 ++++++++--------- 22 files changed, 67 insertions(+), 67 deletions(-) diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index 85eb627390..57121a834b 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -174,7 +174,7 @@ } td.stats { .unit { - font-size: .8em; + font-size: .857em; } } @@ -217,7 +217,7 @@ padding: 0; border: 0; color: $danger-medium; - font-size: 0.929em; + font-size: 1em; cursor: default; } } @@ -267,7 +267,7 @@ ol.category-breadcrumb { margin-bottom: 0; } a.badge-category, .dropdown-header { - font-size: 0.929em; + font-size: 0.857em; font-weight: bold; float: none; text-transform: none; @@ -296,7 +296,7 @@ ol.category-breadcrumb { float: left; margin: 5px 0 10px; .top-date-string { - font-size: 0.7em; + font-size: 0.857em; } } @@ -318,7 +318,7 @@ ol.category-breadcrumb { @include unselectable; - font-size: 1.2em; + font-size: 1.143em; border: 1px solid $primary-low; padding: 5px; background: $secondary; @@ -342,7 +342,7 @@ ol.category-breadcrumb { } .top-date-string { font-weight: normal; - font-size: 0.8em; + font-size: 0.857em; } &:hover { background-color: $highlight-medium; diff --git a/app/assets/stylesheets/common/base/category-list.scss b/app/assets/stylesheets/common/base/category-list.scss index c38bfe61c7..1e01b6268d 100644 --- a/app/assets/stylesheets/common/base/category-list.scss +++ b/app/assets/stylesheets/common/base/category-list.scss @@ -75,7 +75,7 @@ .description { padding: 0 1em 1em 1em; text-align: center; - font-size: 1.05em; + font-size: 1em; color: dark-light-choose($primary-medium, $secondary-high); .overflow { max-height: 6em; @@ -85,7 +85,7 @@ } h3 { - font-size: 1.2em; + font-size: 1.286em; margin-bottom: 0.5em; margin-top: 0.25em; line-height: 1.1em; @@ -102,7 +102,7 @@ } h3 { - font-size: 1.2em; + font-size: 1.286em; text-align: center; } diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 2c9f7e1832..e7a969ac5e 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -300,7 +300,7 @@ color: $primary; } span.name { - font-size: .8em; + font-size: .857em; vertical-align: middle; } &.selected { @@ -326,7 +326,7 @@ div.ac-wrap.disabled { div.ac-wrap div.item a.remove, .remove-link { margin-left: 4px; - font-size: .8em; + font-size: .857em; line-height: 10px; padding: 1.5px 1.5px 1.5px 2.5px; border-radius: 12px; diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss index 6e874e14df..c9092a03b7 100644 --- a/app/assets/stylesheets/common/base/directory.scss +++ b/app/assets/stylesheets/common/base/directory.scss @@ -29,7 +29,7 @@ border-bottom: 1px solid $primary-low; .number, .time-read { - font-size: 1.4em; + font-size: 1.429em; color: $primary-medium; } } diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index fb3825c5f6..ce410bfb13 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -47,11 +47,11 @@ body { } big { - font-size: 2em; + font-size: 2.286em; } small { - font-size: .643em; + font-size: .786em; } //setting a static limit on big and small prevents nesting abuse @@ -423,7 +423,7 @@ select { .content-list { h3 { color: $primary-medium; - font-size: 1.071em; + font-size: 1.143em; padding-left: 5px; margin-bottom: 10px; } @@ -474,7 +474,7 @@ select { .control-label { font-weight: bold; - font-size: 1.2em; + font-size: 1.286em; line-height: 2; } @@ -517,7 +517,7 @@ select { #loading-message { position: absolute; - font-size: 2.143em; + font-size: 2.286em; text-align: center; top: 120px; left: 500px; diff --git a/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss index 2a398d6f10..c842e1f711 100644 --- a/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss +++ b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss @@ -49,7 +49,7 @@ h3 { font-weight: normal; - font-size: 1.071em; + font-size: 1.143em; } } } diff --git a/app/assets/stylesheets/common/base/exception.scss b/app/assets/stylesheets/common/base/exception.scss index 312e87da1e..346a833432 100644 --- a/app/assets/stylesheets/common/base/exception.scss +++ b/app/assets/stylesheets/common/base/exception.scss @@ -7,12 +7,12 @@ height: 60px; } .reason { - font-size: 1.714em; + font-size: 1.857em; height: 24px; } .url { font-style: italic; - font-size: .786em; + font-size: .857em; } .desc { margin-top: 16px; diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index 4b8f48d08b..3f04c28bf1 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -15,7 +15,7 @@ .time, .delete-info { color: lighten($primary, 40%); - font-size: 0.8em; + font-size: 0.857em; } .group-member-info { @@ -28,11 +28,13 @@ display: flex; width: 100%; justify-content: space-between; + .group-post-title { + font-size: 1.143em; + } } .group-post-excerpt { margin: 1em 0; - font-size: 0.929em; word-wrap: break-word; color: $primary; } @@ -45,13 +47,13 @@ width: 100%; .group-info-name { - font-size: 1.4em; + font-size: 1.429em; font-weight: bold; color: $primary; } .group-info-full-name { - font-size: 1.2em; + font-size: 1.286em; color: dark-light-choose($primary-high, $secondary-low); } diff --git a/app/assets/stylesheets/common/base/groups.scss b/app/assets/stylesheets/common/base/groups.scss index c6e7791ab8..6ea6a1a8c9 100644 --- a/app/assets/stylesheets/common/base/groups.scss +++ b/app/assets/stylesheets/common/base/groups.scss @@ -22,7 +22,7 @@ } td.groups-user-count { - font-size: 1.2em; + font-size: 1.286em; } } @@ -38,7 +38,7 @@ } .groups-info-title { - font-size: 0.9em; + font-size: 0.857em; color: dark-light-choose($primary-medium, $secondary-medium); } diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss index c45ce027bf..d774ad0fed 100644 --- a/app/assets/stylesheets/common/base/login.scss +++ b/app/assets/stylesheets/common/base/login.scss @@ -28,7 +28,7 @@ $input-width: 220px; } .disclaimer { - font-size: 0.9em; + font-size: 0.857em; color: dark-light-choose($primary-medium, $secondary-medium); clear: both; } @@ -58,7 +58,7 @@ $input-width: 220px; .instructions { color: dark-light-choose($primary-medium, $secondary-medium); margin: 0; - font-size: 0.929em; + font-size: 0.857em; font-weight: normal; line-height: 18px; } @@ -100,7 +100,7 @@ $input-width: 220px; .instructions { color: dark-light-choose($primary-medium, $secondary-medium); margin: 0; - font-size: 0.929em; + font-size: 0.857em; font-weight: normal; line-height: 18px; } diff --git a/app/assets/stylesheets/common/base/magnific-popup.scss b/app/assets/stylesheets/common/base/magnific-popup.scss index b37047cb0c..c1dfab230b 100644 --- a/app/assets/stylesheets/common/base/magnific-popup.scss +++ b/app/assets/stylesheets/common/base/magnific-popup.scss @@ -285,7 +285,6 @@ button { } padding: 0 0 18px 10px; color: $controls-color; - font-style: normal; font-size: 2em; font-family: Arial, Baskerville, monospace; diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 2aef0117e0..977866908d 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -65,7 +65,7 @@ } .new { - font-size: 0.8em; + font-size: 0.857em; margin-left: 0.5em; color: dark-light-choose($primary-medium, $secondary-medium); } @@ -174,7 +174,7 @@ } li:not(.category):not(.heading) { - font-size: 0.929em; + font-size: 1em; line-height: 16px; .fa { @@ -328,7 +328,7 @@ div.menu-links-header { } a { - font-size: 1.1em; + font-size: 1.143em; } .d-icon-user { margin-right: 0.2em; diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 48a8f67824..30d2fc27e0 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -153,7 +153,7 @@ } p { color: darken($primary, 40%); - font-size: 0.929em; + font-size: 1em; } .archetype-option { margin-bottom: 20px; @@ -247,7 +247,7 @@ margin-left: 0 !important; // override needed font-weight: bold; .topic-title { - font-size: 0.929em; + font-size: 1em; font-weight: normal; } &.btn-reply-here { diff --git a/app/assets/stylesheets/common/base/not-found.scss b/app/assets/stylesheets/common/base/not-found.scss index aa9165f29e..2229548007 100644 --- a/app/assets/stylesheets/common/base/not-found.scss +++ b/app/assets/stylesheets/common/base/not-found.scss @@ -1,7 +1,7 @@ // Page not found styles h1.page-not-found { - font-size: 2.25em; + font-size: 2.286em; line-height: 1.25; } diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 2d68e3a926..8251835934 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -118,7 +118,7 @@ aside.onebox { clear: both; h3, h4 { - font-size: 1.17em; + font-size: 1.143em; margin: 0 0 10px 0; } @@ -395,7 +395,7 @@ aside.onebox.twitterstatus .onebox-body { .album-title { width: 100%; - font-size: 1.083em; + font-size: 1.143em; line-height: 30px; color: #ccc; text-decoration: none; diff --git a/app/assets/stylesheets/common/base/request_access.scss b/app/assets/stylesheets/common/base/request_access.scss index 63c95bc7e5..77fd99202a 100644 --- a/app/assets/stylesheets/common/base/request_access.scss +++ b/app/assets/stylesheets/common/base/request_access.scss @@ -4,10 +4,10 @@ input[type=text] { width: 320px; height: 30px; - font-size: 1.571em; + font-size: 1.429em; } input[type=submit] { - font-size: 1.571em; + font-size: 1.429em; padding: 10px; } } diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index b5a29fba5d..e4a18b18ae 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -36,7 +36,7 @@ } .search-link { .topic-statuses, .topic-title { - font-size: 1.3em; + font-size: 1.286em; line-height: 25px; } @@ -63,7 +63,7 @@ } .discourse-tag { - font-size: 0.8em; + font-size: 0.857em; } } diff --git a/app/assets/stylesheets/common/base/share_link.scss b/app/assets/stylesheets/common/base/share_link.scss index 00cf0ca426..b9c3694119 100644 --- a/app/assets/stylesheets/common/base/share_link.scss +++ b/app/assets/stylesheets/common/base/share_link.scss @@ -22,7 +22,7 @@ margin: 14px 0; } h3 { - font-size: 0.929em; + font-size: 1em; } .copy-text { display: inline-block; @@ -31,7 +31,7 @@ color: $success; opacity: 1; transition: opacity 0.25s; - font-size: .929em; + font-size: 1em; &:not(.success) { opacity: 0; } @@ -40,7 +40,7 @@ margin-left: 2px; margin-right: 8px; float: left; - font-size: 1.571em; + font-size: 1.857em; } .reply-as-new-topic { float: left; diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 6eb3d47d46..46a363c53a 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -23,7 +23,7 @@ } .tag-count { - font-size: 0.9em; + font-size: 0.857em; } } @@ -86,7 +86,7 @@ $tag-color: $primary-medium; .discourse-tag-count { - font-size: 0.8em; + font-size: 0.857em; color: $tag-color; } @@ -153,7 +153,7 @@ $tag-color: $primary-medium; .topic-list-item .discourse-tags { display: block; - font-size: 0.75em; + font-size: 0.786em; font-weight: normal; clear: both; margin-top: 5px; @@ -170,7 +170,7 @@ $tag-color: $primary-medium; .mobile-view .topic-list-item .discourse-tags { display: inline-block; - font-size: 0.9em; + font-size: 0.857em; margin-top: 0; .discourse-tag.box { position:relative; @@ -185,7 +185,7 @@ $tag-color: $primary-medium; font-family: FontAwesome; color: $primary-low-mid; margin-right: 5px; - font-size: 0.7em; + font-size: 0.786em; position:relative; top: -0.1em; } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index de4ed21e79..9197ebfa73 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -371,14 +371,14 @@ blockquote > *:last-child { margin-top: 6px; text-transform: uppercase; font-weight: bold; - font-size: 0.9em; + font-size: 0.857em; color: dark-light-choose($primary-low-mid, $secondary-high); .custom-message { text-transform: none; margin: 15px 0px 5px; font-weight: normal; - font-size: 1.11em; + font-size: 1.143em; p { margin: 5px 0; } @@ -464,7 +464,7 @@ a.mention, a.mention-group { > span.help { display: inline-block; color: dark-light-choose($primary-medium, $secondary-medium); - font-size: 0.929em; + font-size: 0.857em; font-style: italic; line-height: $base-line-height; margin-bottom: 1px; diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 4c275a0598..61a6179fb8 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -60,7 +60,7 @@ .show-badge .badge-user-info { .earned { - font-size: 1.3em; + font-size: 1.286em; margin-bottom: 1em; } } @@ -71,7 +71,7 @@ .load-more { padding-top: 30px; display: block; - font-size: 1.2em; + font-size: 1.286em; } } @@ -86,11 +86,11 @@ } .date { display: inline-block; - font-size: 1.1em; + font-size: 1.143em; margin-left: 10px; } .post-link { - font-size: 1.3em; + font-size: 1.286em; width: 500px; margin: 0; padding: 0; @@ -130,7 +130,7 @@ top: 5px; font-weight: bold; color: $primary-medium; - font-size: 1.2em; + font-size: 1.286em; } .badge-contents { diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index d3a415b75e..1d4373cc96 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -19,7 +19,6 @@ width: 100%; .secondary { - font-size: 0.929em; .btn { padding: 3px 12px; @@ -61,13 +60,13 @@ } h1 { - font-size: 2.143em; + font-size: 2.286em; font-weight: normal; i {font-size: .8em;} } h2 { - font-size: 1.214em; + font-size: 1.286em; font-weight: normal; margin-top: 10px; max-width: 100%; @@ -168,7 +167,7 @@ } h2 { - font-size: 1.071em; + font-size: 1.143em; margin-top: 4px; } @@ -256,7 +255,7 @@ color: dark-light-choose($primary-medium, $secondary-medium); margin-top: 5px; margin-bottom: 10px; - font-size: 80%; + font-size: .857em; line-height: 1.4em; } } @@ -268,7 +267,7 @@ text-align: top; color: $danger; font-weight: bold; - font-size: 1.3em; + font-size: 1.286em; } } @@ -403,7 +402,7 @@ .value { font-weight: bold; - font-size: 1.2em; + font-size: 1.286em; } .label { @@ -450,7 +449,7 @@ .links-section { .domain { - font-size: 0.714em; + font-size: 0.786em; color: dark-light-choose($primary-medium, $secondary-high); } } @@ -480,7 +479,7 @@ .instructions { color: dark-light-choose($primary-medium, $secondary-medium); margin-bottom: 10px; - font-size: 80%; + font-size: .857em; line-height: 1.4em; a[href] { From 2908aab0da1700c924e99fd37715e6f583fb3229 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Dec 2017 14:31:28 -0500 Subject: [PATCH 023/106] Allow extensibility on username route format (non-english usernames) --- .../components/user-card-contents.js.es6 | 3 +- config/routes.rb | 151 +++++++++--------- lib/route_formats.rb | 11 ++ 3 files changed, 85 insertions(+), 80 deletions(-) create mode 100644 lib/route_formats.rb diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 index 238cd97495..b7d3b4d31f 100644 --- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 +++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 @@ -115,8 +115,7 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, { return false; } - // XSS protection (should be encapsulated) - username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, ""); + username = Ember.Handlebars.Utils.escapeExpression(username.toString()); // Don't show on mobile if (this.site.mobileView) { diff --git a/config/routes.rb b/config/routes.rb index 06962901e0..08d7dbd38f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,12 +4,7 @@ require_dependency "admin_constraint" require_dependency "staff_constraint" require_dependency "homepage_constraint" require_dependency "permalink_constraint" - -# This used to be User#username_format, but that causes a preload of the User object -# and makes Guard not work properly. -USERNAME_ROUTE_FORMAT = /[\w.\-]+?/ unless defined? USERNAME_ROUTE_FORMAT - -BACKUP_ROUTE_FORMAT = /.+\.(sql\.gz|tar\.gz|tgz)/i unless defined? BACKUP_ROUTE_FORMAT +require_dependency "route_formats" Discourse::Application.routes.draw do @@ -95,7 +90,7 @@ Discourse::Application.routes.draw do get "moderation_history" => "moderation_history#index" - resources :users, id: USERNAME_ROUTE_FORMAT, except: [:show] do + resources :users, id: RouteFormats.username, except: [:show] do collection do get "list" => "users#index" get "list/:query" => "users#index" @@ -133,7 +128,7 @@ Discourse::Application.routes.draw do post "reset_bounce_score" end get "users/:id.json" => 'users#show', defaults: { format: 'json' } - get 'users/:id/:username' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT } + get 'users/:id/:username' => 'users#show', constraints: { username: RouteFormats.username } get 'users/:id/:username/badges' => 'users#show' get 'users/:id/:username/tl3_requirements' => 'users#show' @@ -254,10 +249,10 @@ Discourse::Application.routes.draw do resources :backups, only: [:index, :create], constraints: AdminConstraint.new do member do - get "" => "backups#show", constraints: { id: BACKUP_ROUTE_FORMAT } - put "" => "backups#email", constraints: { id: BACKUP_ROUTE_FORMAT } - delete "" => "backups#destroy", constraints: { id: BACKUP_ROUTE_FORMAT } - post "restore" => "backups#restore", constraints: { id: BACKUP_ROUTE_FORMAT } + get "" => "backups#show", constraints: { id: RouteFormats.backup } + put "" => "backups#email", constraints: { id: RouteFormats.backup } + delete "" => "backups#destroy", constraints: { id: RouteFormats.backup } + post "restore" => "backups#restore", constraints: { id: RouteFormats.backup } end collection do get "logs" => "backups#logs" @@ -291,7 +286,7 @@ Discourse::Application.routes.draw do get "extra-locales/:bundle" => "extra_locales#show" - resources :session, id: USERNAME_ROUTE_FORMAT, only: [:create, :destroy, :become] do + resources :session, id: RouteFormats.username, only: [:create, :destroy, :become] do get 'become' collection do post "forgot_password" @@ -354,64 +349,64 @@ Discourse::Application.routes.draw do constraints: { token: /[0-9a-f]+/ } }.merge(index == 1 ? { as: 'confirm_admin' } : {})) post "#{root_path}/confirm-admin/:token" => "users#confirm_admin", constraints: { token: /[0-9a-f]+/ } - get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT, group_name: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT, group_name: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username.json" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT }, defaults: { format: :json } - get({ "#{root_path}/:username" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT, format: /(json|html)/ } }.merge(index == 1 ? { as: 'user' } : {})) - put "#{root_path}/:username" => "users#update", constraints: { username: USERNAME_ROUTE_FORMAT }, defaults: { format: :json } - get "#{root_path}/:username/emails" => "users#check_emails", constraints: { username: USERNAME_ROUTE_FORMAT } - get({ "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } }.merge(index == 1 ? { as: :email_preferences } : {})) - get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/about-me" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } - put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: USERNAME_ROUTE_FORMAT } - delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: USERNAME_ROUTE_FORMAT } - put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/preferences/card-badge" => "users#card_badge", constraints: { username: USERNAME_ROUTE_FORMAT } - put "#{root_path}/:username/preferences/card-badge" => "users#update_card_badge", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/summary" => "users#summary", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/invited" => "users#invited", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormats.username, group_name: RouteFormats.username } + get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: { username: RouteFormats.username, group_name: RouteFormats.username } + get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormats.username }, defaults: { format: :json } + get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormats.username, format: /(json|html)/ } }.merge(index == 1 ? { as: 'user' } : {})) + put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormats.username }, defaults: { format: :json } + get "#{root_path}/:username/emails" => "users#check_emails", constraints: { username: RouteFormats.username } + get({ "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: RouteFormats.username } }.merge(index == 1 ? { as: :email_preferences } : {})) + get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: RouteFormats.username } + put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/about-me" => "users#preferences", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: { username: RouteFormats.username } + put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: RouteFormats.username } + put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: RouteFormats.username } + delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormats.username } + put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/preferences/card-badge" => "users#card_badge", constraints: { username: RouteFormats.username } + put "#{root_path}/:username/preferences/card-badge" => "users#update_card_badge", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: RouteFormats.username } post "#{root_path}/action/send_activation_email" => "users#send_activation_email" - get "#{root_path}/:username/summary" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/activity" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/badges" => "users#badges", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/notifications" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/activity/pending" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - delete "#{root_path}/:username" => "users#destroy", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/summary" => "users#show", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: RouteFormats.username } + get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: { username: RouteFormats.username } + get "#{root_path}/:username/activity" => "users#show", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/badges" => "users#badges", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/notifications" => "users#show", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/activity/pending" => "users#show", constraints: { username: RouteFormats.username } + delete "#{root_path}/:username" => "users#destroy", constraints: { username: RouteFormats.username } get "#{root_path}/by-external/:external_id" => "users#show", constraints: { external_id: /[^\/]+/ } - get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } - get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormats.username } end - get "user-badges/:username.json" => "user_badges#username", constraints: { username: USERNAME_ROUTE_FORMAT }, defaults: { format: :json } - get "user-badges/:username" => "user_badges#username", constraints: { username: USERNAME_ROUTE_FORMAT } + get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormats.username }, defaults: { format: :json } + get "user-badges/:username" => "user_badges#username", constraints: { username: RouteFormats.username } - post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: { username: USERNAME_ROUTE_FORMAT } - get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT } - get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT } + post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: { username: RouteFormats.username } + get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormats.username } + get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormats.username } # in most production settings this is bypassed get "letter_avatar_proxy/:version/letter/:letter/:color/:size.png" => "user_avatars#show_proxy_letter" @@ -436,10 +431,10 @@ Discourse::Application.routes.draw do get "posts/by_number/:topic_id/:post_number" => "posts#by_number" get "posts/:id/reply-history" => "posts#reply_history" get "posts/:id/reply-ids" => "posts#reply_ids" - get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: USERNAME_ROUTE_FORMAT } - get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: USERNAME_ROUTE_FORMAT } + get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: RouteFormats.username } + get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: RouteFormats.username } - resources :groups, id: USERNAME_ROUTE_FORMAT do + resources :groups, id: RouteFormats.username do get "posts.rss" => "groups#posts_feed", format: :rss get "mentions.rss" => "groups#mentions_feed", format: :rss @@ -585,19 +580,19 @@ Discourse::Application.routes.draw do resources :similar_topics get "topics/feature_stats" - get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: { username: USERNAME_ROUTE_FORMAT } - get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } - get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: { username: USERNAME_ROUTE_FORMAT } - get "topics/private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", constraints: { username: USERNAME_ROUTE_FORMAT } - get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: { username: USERNAME_ROUTE_FORMAT } + get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: { username: RouteFormats.username } + get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: { username: RouteFormats.username } + get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: { username: RouteFormats.username } + get "topics/private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", constraints: { username: RouteFormats.username } + get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: { username: RouteFormats.username } get "topics/private-messages-group/:username/:group_name.json" => "list#private_messages_group", as: "topics_private_messages_group", constraints: { - username: USERNAME_ROUTE_FORMAT, - group_name: USERNAME_ROUTE_FORMAT + username: RouteFormats.username, + group_name: RouteFormats.username } get "topics/private-messages-group/:username/:group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive", constraints: { - username: USERNAME_ROUTE_FORMAT, - group_name: USERNAME_ROUTE_FORMAT + username: RouteFormats.username, + group_name: RouteFormats.username } get 'embed/comments' => 'embed#comments' diff --git a/lib/route_formats.rb b/lib/route_formats.rb new file mode 100644 index 0000000000..9278f879e3 --- /dev/null +++ b/lib/route_formats.rb @@ -0,0 +1,11 @@ +module RouteFormats + + def self.username + /[\w.\-]+?/ + end + + def self.backup + /.+\.(sql\.gz|tar\.gz|tgz)/i + end + +end From 9a514e6a2651e0a79499225d279fb98bf67dc1f3 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 21 Dec 2017 14:46:29 -0500 Subject: [PATCH 024/106] UX: Fixing mobile post action size --- app/assets/stylesheets/mobile/topic-post.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index b012356eb1..49ec7ce78e 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -33,6 +33,7 @@ span.badge-posts { } button { border: none; + font-size: 1.286em; padding: 8px 10px; vertical-align: top; background: transparent; From 69a90f31fba1b6dedfeefbcd5f8a7efb289f6611 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Dec 2017 15:21:28 -0500 Subject: [PATCH 025/106] FEATURE: Allow Forums to disable the Backups feature --- app/assets/javascripts/admin/routes/admin-backups.js.es6 | 2 +- app/assets/javascripts/admin/templates/admin.hbs | 4 +++- app/controllers/admin/backups_controller.rb | 5 +++++ app/jobs/scheduled/schedule_backup.rb | 2 +- config/locales/server.en.yml | 1 + config/site_settings.yml | 4 ++++ spec/requests/admin/backups_controller_spec.rb | 8 ++++++++ 7 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index 6d1a425190..92ac4652fa 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -68,7 +68,7 @@ export default Discourse.Route.extend({ function(confirmed) { if (confirmed) { backup.destroy().then(function() { - self.controllerFor("adminBackupsIndex").removeObject(backup); + self.controllerFor("adminBackupsIndex").get('model').removeObject(backup); }); } } diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs index 01f9ffb673..af4aafde94 100644 --- a/app/assets/javascripts/admin/templates/admin.hbs +++ b/app/assets/javascripts/admin/templates/admin.hbs @@ -20,7 +20,9 @@ {{#if currentUser.admin}} {{nav-item route='adminCustomize' label='admin.customize.title'}} {{nav-item route='adminApi' label='admin.api.title'}} - {{nav-item route='admin.backups' label='admin.backups.title'}} + {{#if siteSettings.enable_backups}} + {{nav-item route='admin.backups' label='admin.backups.title'}} + {{/if}} {{/if}} {{nav-item route='adminPlugins' label='admin.plugins.title'}} {{plugin-outlet name="admin-menu" connectorTagName="li"}} diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb index 7a3fcfa78d..52b8403cdd 100644 --- a/app/controllers/admin/backups_controller.rb +++ b/app/controllers/admin/backups_controller.rb @@ -2,6 +2,7 @@ require "backup_restore/backup_restore" class Admin::BackupsController < Admin::AdminController + before_action :ensure_backups_enabled skip_before_action :check_xhr, only: [:index, :show, :logs, :check_backup_chunk, :upload_backup_chunk] def index @@ -178,4 +179,8 @@ class Admin::BackupsController < Admin::AdminController `df -Pk #{Rails.root}/public/backups | awk 'NR==2 {print $4 * 1024;}'`.to_i > size end + def ensure_backups_enabled + raise Discourse::InvalidAccess.new unless SiteSetting.enable_backups? + end + end diff --git a/app/jobs/scheduled/schedule_backup.rb b/app/jobs/scheduled/schedule_backup.rb index 047c8a1141..48ebe73fc2 100644 --- a/app/jobs/scheduled/schedule_backup.rb +++ b/app/jobs/scheduled/schedule_backup.rb @@ -5,7 +5,7 @@ module Jobs sidekiq_options retry: false def execute(args) - return unless SiteSetting.automatic_backups_enabled? + return unless SiteSetting.enable_backups? && SiteSetting.automatic_backups_enabled? if latest_backup = Backup.all[0] date = File.ctime(latest_backup.path).getutc.to_date diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 40791ae9f8..b6e5af9b2e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1159,6 +1159,7 @@ en: github_client_secret: "Client secret for Github authentication, registered at https://github.com/settings/applications" readonly_mode_during_backup: "Enable read only mode while taking a backup" + enable_backups: "Allow administrators to create backups of the forum" allow_restore: "Allow restore, which can replace ALL site data! Leave false unless you plan to restore a backup" maximum_backups: "The maximum amount of backups to keep on disk. Older backups are automatically deleted" automatic_backups_enabled: "Run automatic backups as defined in backup frequency" diff --git a/config/site_settings.yml b/config/site_settings.yml index af3d8beacd..44ad6c6014 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1168,6 +1168,10 @@ legal: default: '' backups: + enable_backups: + default: true + shadowed_by_global: true + client: true readonly_mode_during_backup: default: false allow_restore: diff --git a/spec/requests/admin/backups_controller_spec.rb b/spec/requests/admin/backups_controller_spec.rb index 1fc67d7601..719056f64b 100644 --- a/spec/requests/admin/backups_controller_spec.rb +++ b/spec/requests/admin/backups_controller_spec.rb @@ -7,6 +7,14 @@ RSpec.describe Admin::BackupsController do sign_in(admin) end + describe "#index" do + it "raises an error when backups are disabled" do + SiteSetting.enable_backups = false + get "/admin/backups.json" + expect(response).not_to be_success + end + end + describe '#rollback' do it 'should rollback the restore' do BackupRestore.expects(:rollback!) From 063e449ce520863771eca78db0c97e5b6d841c26 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Dec 2017 15:30:32 -0500 Subject: [PATCH 026/106] FIX: `RouteFormat` is a better class name than `RouteFormats` --- config/routes.rb | 146 +++++++++++----------- lib/{route_formats.rb => route_format.rb} | 2 +- 2 files changed, 74 insertions(+), 74 deletions(-) rename lib/{route_formats.rb => route_format.rb} (84%) diff --git a/config/routes.rb b/config/routes.rb index 08d7dbd38f..468baf467e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,7 @@ require_dependency "admin_constraint" require_dependency "staff_constraint" require_dependency "homepage_constraint" require_dependency "permalink_constraint" -require_dependency "route_formats" +require_dependency "route_format" Discourse::Application.routes.draw do @@ -90,7 +90,7 @@ Discourse::Application.routes.draw do get "moderation_history" => "moderation_history#index" - resources :users, id: RouteFormats.username, except: [:show] do + resources :users, id: RouteFormat.username, except: [:show] do collection do get "list" => "users#index" get "list/:query" => "users#index" @@ -128,7 +128,7 @@ Discourse::Application.routes.draw do post "reset_bounce_score" end get "users/:id.json" => 'users#show', defaults: { format: 'json' } - get 'users/:id/:username' => 'users#show', constraints: { username: RouteFormats.username } + get 'users/:id/:username' => 'users#show', constraints: { username: RouteFormat.username } get 'users/:id/:username/badges' => 'users#show' get 'users/:id/:username/tl3_requirements' => 'users#show' @@ -249,10 +249,10 @@ Discourse::Application.routes.draw do resources :backups, only: [:index, :create], constraints: AdminConstraint.new do member do - get "" => "backups#show", constraints: { id: RouteFormats.backup } - put "" => "backups#email", constraints: { id: RouteFormats.backup } - delete "" => "backups#destroy", constraints: { id: RouteFormats.backup } - post "restore" => "backups#restore", constraints: { id: RouteFormats.backup } + get "" => "backups#show", constraints: { id: RouteFormat.backup } + put "" => "backups#email", constraints: { id: RouteFormat.backup } + delete "" => "backups#destroy", constraints: { id: RouteFormat.backup } + post "restore" => "backups#restore", constraints: { id: RouteFormat.backup } end collection do get "logs" => "backups#logs" @@ -286,7 +286,7 @@ Discourse::Application.routes.draw do get "extra-locales/:bundle" => "extra_locales#show" - resources :session, id: RouteFormats.username, only: [:create, :destroy, :become] do + resources :session, id: RouteFormat.username, only: [:create, :destroy, :become] do get 'become' collection do post "forgot_password" @@ -349,64 +349,64 @@ Discourse::Application.routes.draw do constraints: { token: /[0-9a-f]+/ } }.merge(index == 1 ? { as: 'confirm_admin' } : {})) post "#{root_path}/confirm-admin/:token" => "users#confirm_admin", constraints: { token: /[0-9a-f]+/ } - get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormats.username, group_name: RouteFormats.username } - get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: { username: RouteFormats.username, group_name: RouteFormats.username } - get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormats.username }, defaults: { format: :json } - get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormats.username, format: /(json|html)/ } }.merge(index == 1 ? { as: 'user' } : {})) - put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormats.username }, defaults: { format: :json } - get "#{root_path}/:username/emails" => "users#check_emails", constraints: { username: RouteFormats.username } - get({ "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: RouteFormats.username } }.merge(index == 1 ? { as: :email_preferences } : {})) - get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: RouteFormats.username } - put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/about-me" => "users#preferences", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: { username: RouteFormats.username } - put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: RouteFormats.username } - put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: RouteFormats.username } - delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormats.username } - put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/preferences/card-badge" => "users#card_badge", constraints: { username: RouteFormats.username } - put "#{root_path}/:username/preferences/card-badge" => "users#update_card_badge", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username } + get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username } + get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormat.username }, defaults: { format: :json } + get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormat.username, format: /(json|html)/ } }.merge(index == 1 ? { as: 'user' } : {})) + put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormat.username }, defaults: { format: :json } + get "#{root_path}/:username/emails" => "users#check_emails", constraints: { username: RouteFormat.username } + get({ "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: RouteFormat.username } }.merge(index == 1 ? { as: :email_preferences } : {})) + get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: RouteFormat.username } + put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/about-me" => "users#preferences", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: { username: RouteFormat.username } + put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: RouteFormat.username } + put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: RouteFormat.username } + delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username } + put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/card-badge" => "users#card_badge", constraints: { username: RouteFormat.username } + put "#{root_path}/:username/preferences/card-badge" => "users#update_card_badge", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: RouteFormat.username } post "#{root_path}/action/send_activation_email" => "users#send_activation_email" - get "#{root_path}/:username/summary" => "users#show", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: RouteFormats.username } - get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: { username: RouteFormats.username } - get "#{root_path}/:username/activity" => "users#show", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/badges" => "users#badges", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/notifications" => "users#show", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/activity/pending" => "users#show", constraints: { username: RouteFormats.username } - delete "#{root_path}/:username" => "users#destroy", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/summary" => "users#show", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: RouteFormat.username } + get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: { username: RouteFormat.username } + get "#{root_path}/:username/activity" => "users#show", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/badges" => "users#badges", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/notifications" => "users#show", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/activity/pending" => "users#show", constraints: { username: RouteFormat.username } + delete "#{root_path}/:username" => "users#destroy", constraints: { username: RouteFormat.username } get "#{root_path}/by-external/:external_id" => "users#show", constraints: { external_id: /[^\/]+/ } - get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormats.username } - get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormats.username } + get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormat.username } end - get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormats.username }, defaults: { format: :json } - get "user-badges/:username" => "user_badges#username", constraints: { username: RouteFormats.username } + get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormat.username }, defaults: { format: :json } + get "user-badges/:username" => "user_badges#username", constraints: { username: RouteFormat.username } - post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: { username: RouteFormats.username } - get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormats.username } - get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormats.username } + post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: { username: RouteFormat.username } + get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormat.username } + get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormat.username } # in most production settings this is bypassed get "letter_avatar_proxy/:version/letter/:letter/:color/:size.png" => "user_avatars#show_proxy_letter" @@ -431,10 +431,10 @@ Discourse::Application.routes.draw do get "posts/by_number/:topic_id/:post_number" => "posts#by_number" get "posts/:id/reply-history" => "posts#reply_history" get "posts/:id/reply-ids" => "posts#reply_ids" - get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: RouteFormats.username } - get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: RouteFormats.username } + get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: RouteFormat.username } + get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: RouteFormat.username } - resources :groups, id: RouteFormats.username do + resources :groups, id: RouteFormat.username do get "posts.rss" => "groups#posts_feed", format: :rss get "mentions.rss" => "groups#mentions_feed", format: :rss @@ -580,19 +580,19 @@ Discourse::Application.routes.draw do resources :similar_topics get "topics/feature_stats" - get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: { username: RouteFormats.username } - get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: { username: RouteFormats.username } - get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: { username: RouteFormats.username } - get "topics/private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", constraints: { username: RouteFormats.username } - get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: { username: RouteFormats.username } + get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: { username: RouteFormat.username } + get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: { username: RouteFormat.username } + get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: { username: RouteFormat.username } + get "topics/private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", constraints: { username: RouteFormat.username } + get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: { username: RouteFormat.username } get "topics/private-messages-group/:username/:group_name.json" => "list#private_messages_group", as: "topics_private_messages_group", constraints: { - username: RouteFormats.username, - group_name: RouteFormats.username + username: RouteFormat.username, + group_name: RouteFormat.username } get "topics/private-messages-group/:username/:group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive", constraints: { - username: RouteFormats.username, - group_name: RouteFormats.username + username: RouteFormat.username, + group_name: RouteFormat.username } get 'embed/comments' => 'embed#comments' diff --git a/lib/route_formats.rb b/lib/route_format.rb similarity index 84% rename from lib/route_formats.rb rename to lib/route_format.rb index 9278f879e3..0ebd43fbdf 100644 --- a/lib/route_formats.rb +++ b/lib/route_format.rb @@ -1,4 +1,4 @@ -module RouteFormats +module RouteFormat def self.username /[\w.\-]+?/ From aed37770e369a5053f5e58ea56ce73dd9692b7dd Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Dec 2017 16:29:11 -0500 Subject: [PATCH 027/106] FIX: Load the route format before discourse --- config/routes.rb | 1 - lib/discourse.rb | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 468baf467e..46ade1df6d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,6 @@ require_dependency "admin_constraint" require_dependency "staff_constraint" require_dependency "homepage_constraint" require_dependency "permalink_constraint" -require_dependency "route_format" Discourse::Application.routes.draw do diff --git a/lib/discourse.rb b/lib/discourse.rb index 18231f3d0a..66ce5c3d76 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -1,5 +1,6 @@ require 'cache' require 'open3' +require_dependency 'route_format' require_dependency 'plugin/instance' require_dependency 'auth/default_current_user_provider' require_dependency 'version' From fc2f948a39c2a9e2420aac8ea9a9930ab23bab70 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 21 Dec 2017 16:31:45 -0500 Subject: [PATCH 028/106] UX: user menu-panel icons too large --- app/assets/stylesheets/common/base/menu-panel.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 977866908d..4c4fe1279b 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -327,9 +327,6 @@ div.menu-links-header { color: dark-light-choose($primary-medium, $secondary-medium); } - a { - font-size: 1.143em; - } .d-icon-user { margin-right: 0.2em; } From e667434bb3ff68b31635d0212fa12a0b3e5102ee Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 21 Dec 2017 17:13:06 -0500 Subject: [PATCH 029/106] UX: Cleaning up a few admin styles --- .../admin/templates/customize-themes-show.hbs | 4 ++-- app/assets/stylesheets/common/admin/admin_base.scss | 11 +++++++---- app/assets/stylesheets/common/admin/customize.scss | 4 ++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index 6f8ac614aa..7f339d63d7 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -1,5 +1,5 @@
-

+

{{#if editingName}} {{text-field value=model.name autofocus="true"}} {{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}} @@ -7,7 +7,7 @@ {{else}} {{model.name}} {{d-icon "pencil"}} {{/if}} -

+ {{#if model.remote_theme}}

diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index dab0f7a58d..e66c539e39 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -363,12 +363,15 @@ $mobile-breakpoint: 700px; .groups, .badges, .web-hook-container { .form-horizontal { - label { - font-weight: bold; + & > div { + margin-bottom: 20px; } - & > div { - margin-top: 10px; + .d-editor-textarea-wrapper { + max-width: 60%; + .d-editor-button-bar { + overflow: hidden; + } } input, textarea, select, .select-box { diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index efdbd08a6b..d39b9ababe 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -4,6 +4,10 @@ margin-bottom: 10px; input { margin-bottom: 0; + font-size: 1rem; + } + .btn-small { + font-size: 1rem; } } From d0b44520bd36e271e0b935a6bd11fc9dc66a19d5 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Dec 2017 17:17:25 -0500 Subject: [PATCH 030/106] Put back removed constants with deprecations for plugin compatibility --- config/routes.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 46ade1df6d..f3cb50b6fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,10 @@ require_dependency "staff_constraint" require_dependency "homepage_constraint" require_dependency "permalink_constraint" +# The following constants have been replaced with `RouteFormat` and are deprecated. +USERNAME_ROUTE_FORMAT = /[\w.\-]+?/ unless defined? USERNAME_ROUTE_FORMAT +BACKUP_ROUTE_FORMAT = /.+\.(sql\.gz|tar\.gz|tgz)/i unless defined? BACKUP_ROUTE_FORMAT + Discourse::Application.routes.draw do match "/404", to: "exceptions#not_found", via: [:get, :post] From 6f89db4c24c32b56724453d327f5ca207aae1bce Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 22 Dec 2017 09:11:49 +0800 Subject: [PATCH 031/106] Re-enable check for yarn when running qunit:test rake task. --- lib/tasks/qunit.rake | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index 3c52e5430a..c97b26d9bc 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -13,11 +13,11 @@ task "qunit:test", [:timeout, :qunit_path] => :environment do |_, args| abort "Chrome 59 or higher is required to run tests in headless mode." end - # unless system("command -v yarn >/dev/null;") - # abort "Yarn is not installed. Download from https://yarnpkg.com/lang/en/docs/install/" - # end - # - # system("yarn install --dev") + unless system("command -v yarn >/dev/null;") + abort "Yarn is not installed. Download from https://yarnpkg.com/lang/en/docs/install/" + end + + system("yarn install --dev") # ensure we have this port available def port_available?(port) From ee1b90503c7ff01f7aa9605f4cdb4308b32b773a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 22 Dec 2017 09:29:35 +0800 Subject: [PATCH 032/106] FIX: Error when an invalid date is passed to certificate generator. https://meta.discourse.org/t/broken-image-in-discobot-certificate-with-no-logo-small-url/76594/2 --- .../certificate_generator.rb | 14 +++++++++++++- .../spec/integration/discobot_certificate_spec.rb | 13 ------------- .../spec/lib/certificate_generator_spec.rb | 11 +++++++++++ 3 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb index da4a9435ff..639aaa7062 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb @@ -2,7 +2,19 @@ module DiscourseNarrativeBot class CertificateGenerator def initialize(user, date) @user = user - @date = I18n.l(Date.parse(date), format: :date_only) + + date = + begin + Date.parse(date) + rescue ArgumentError => e + if e.message == 'invalid date' + Date.parse(Date.today.to_s) + else + raise e + end + end + + @date = I18n.l(date, format: :date_only) @discobot_user = User.find(-2) end diff --git a/plugins/discourse-narrative-bot/spec/integration/discobot_certificate_spec.rb b/plugins/discourse-narrative-bot/spec/integration/discobot_certificate_spec.rb index cf81fce703..5e2f58d84e 100644 --- a/plugins/discourse-narrative-bot/spec/integration/discobot_certificate_spec.rb +++ b/plugins/discourse-narrative-bot/spec/integration/discobot_certificate_spec.rb @@ -33,18 +33,5 @@ describe "Discobot Certificate" do end end end - - describe 'when date is invalid' do - it 'should raise the right error' do - expect do - get '/discobot/certificate.svg', params: { - name: user.name, - date: "", - avatar_url: 'https://somesite.com/someavatar', - user_id: user.id - } - end.to raise_error(ArgumentError, 'invalid date') - end - end end end diff --git a/plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb b/plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb new file mode 100644 index 0000000000..2e3536d746 --- /dev/null +++ b/plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +RSpec.describe DiscourseNarrativeBot::CertificateGenerator do + let(:user) { Fabricate(:user) } + + describe 'when an invalid date is given' do + it 'should default to the current date' do + expect { described_class.new(user, "2017-00-10") }.to_not raise_error + end + end +end From 32171ad286c92f82ec9fe28f520a096cac37695d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 22 Dec 2017 09:30:24 +0800 Subject: [PATCH 033/106] FIX: Invalid logo in discobot's certificate when `SiteSetting.logo_small_url` is blank. https://meta.discourse.org/t/broken-image-in-discobot-certificate-with-no-logo-small-url/76594/2 --- .../certificate_generator.rb | 2 ++ .../spec/lib/certificate_generator_spec.rb | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb index 639aaa7062..bb1f4dbeff 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/certificate_generator.rb @@ -568,6 +568,8 @@ module DiscourseNarrativeBot end def logo_group(size, width, height) + return unless SiteSetting.logo_small_url.present? + begin uri = URI(SiteSetting.logo_small_url) diff --git a/plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb b/plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb index 2e3536d746..69f7e5e848 100644 --- a/plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb +++ b/plugins/discourse-narrative-bot/spec/lib/certificate_generator_spec.rb @@ -8,4 +8,17 @@ RSpec.describe DiscourseNarrativeBot::CertificateGenerator do expect { described_class.new(user, "2017-00-10") }.to_not raise_error end end + + describe '#logo_group' do + describe 'when SiteSetting.logo_small_url is blank' do + before do + SiteSetting.logo_small_url = '' + end + + it 'should not try to fetch a image' do + expect(described_class.new(user, "2017-02-10").send(:logo_group, 1, 1, 1)) + .to eq(nil) + end + end + end end From 2a8da9a9cb026f73f27d97b180132243f0a5474d Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 21 Dec 2017 18:36:02 -0800 Subject: [PATCH 034/106] minor copyedit on google id conflict message --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 72d07f2275..4a4d5520df 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -139,7 +139,7 @@ en: max_username_length_range: "You cannot set the maximum below the minimum." default_categories_already_selected: "You cannot select a category used in another list." s3_upload_bucket_is_required: "You cannot enable uploads to S3 unless you've provided the 's3_upload_bucket'." - conflicting_google_user_id: 'The Google Account ID for this account has changed, for protection this requires manual intervention. Please contact the site administrator with the following reference:
https://meta.discourse.org/t/76575' + conflicting_google_user_id: 'The Google Account ID for this account has changed; this requires staff intervention for security reasons. Please contact staff and point them to
https://meta.discourse.org/t/76575' activemodel: errors: From 3bc53f29469ed6c1a3cb8ee7b8345425f8d334fe Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 21 Dec 2017 18:37:14 -0800 Subject: [PATCH 035/106] very minor copyedit --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4a4d5520df..8817422fa2 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -139,7 +139,7 @@ en: max_username_length_range: "You cannot set the maximum below the minimum." default_categories_already_selected: "You cannot select a category used in another list." s3_upload_bucket_is_required: "You cannot enable uploads to S3 unless you've provided the 's3_upload_bucket'." - conflicting_google_user_id: 'The Google Account ID for this account has changed; this requires staff intervention for security reasons. Please contact staff and point them to
https://meta.discourse.org/t/76575' + conflicting_google_user_id: 'The Google Account ID for this account has changed; staff intervention is required for security reasons. Please contact staff and point them to
https://meta.discourse.org/t/76575' activemodel: errors: From 4935ae433891ff45a35b27a3a571e78b55edd9b5 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 22 Dec 2017 09:28:24 +0530 Subject: [PATCH 036/106] Remove unwanted spaces between HTML tags and support Word documents --- .../discourse/components/d-editor.js.es6 | 2 - .../discourse/lib/to-markdown.js.es6 | 49 ++++++++++++++----- test/javascripts/lib/to-markdown-test.js.es6 | 26 +++++----- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index f7ff2e51c1..c0454cd6c8 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -662,8 +662,6 @@ export default Ember.Component.extend({ if (table) { this.appEvents.trigger('composer:insert-text', table); handled = true; - } else if (html && html.includes("urn:schemas-microsoft-com:office:word")) { - html = ""; // use plain text data for microsoft word } } diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 index 1d2f2cbe25..22eb2b56d6 100644 --- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 +++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 @@ -38,15 +38,15 @@ class Tag { } static emphases() { - return [ ["b", "**"], ["strong", "**"], ["i", "_"], ["em", "_"], ["s", "~~"], ["strike", "~~"] ]; + return [ ["b", "**"], ["strong", "**"], ["i", "*"], ["em", "*"], ["s", "~~"], ["strike", "~~"] ]; } static slices() { - return ["dt", "dd", "tr", "thead", "tbody", "tfoot"]; + return ["dt", "dd", "thead", "tbody", "tfoot"]; } static trimmable() { - return [...Tag.blocks(), ...Tag.headings(), ...Tag.slices(), "li", "td", "th", "br", "hr", "blockquote", "table", "ol"]; + return [...Tag.blocks(), ...Tag.headings(), ...Tag.slices(), "li", "td", "th", "br", "hr", "blockquote", "table", "ol", "tr"]; } static block(name, prefix, suffix) { @@ -73,14 +73,17 @@ class Tag { } decorate(text) { - text = text.trim(); - if (text.includes("\n")) { this.prefix = `<${this.name}>`; this.suffix = ``; } - return super.decorate(text); + let space = text.match(/^\s/) || [""]; + this.prefix = space[0] + this.prefix; + space = text.match(/\s$/) || [""]; + this.suffix = this.suffix + space[0]; + + return super.decorate(text.trim()); } }; } @@ -182,10 +185,6 @@ class Tag { throw "Unsupported format inside Markdown table cells"; } - if (!this.element.next) { - this.suffix = "|"; - } - return this.decorate(text); } }; @@ -268,6 +267,17 @@ class Tag { }; } + static tr() { + return class extends Tag.slice("tr", "|\n") { + decorate(text) { + if (!this.element.next) { + this.suffix = "|"; + } + return `${text}${this.suffix}`; + } + }; + } + } const tags = [ @@ -278,7 +288,7 @@ const tags = [ Tag.cell("td"), Tag.cell("th"), Tag.replace("br", "\n"), Tag.replace("hr", "\n---\n"), Tag.replace("head", ""), Tag.keep("ins"), Tag.keep("del"), Tag.keep("small"), Tag.keep("big"), - Tag.li(), Tag.link(), Tag.image(), Tag.code(), Tag.blockquote(), Tag.table(),, Tag.ol(), + Tag.li(), Tag.link(), Tag.image(), Tag.code(), Tag.blockquote(), Tag.table(), Tag.ol(), Tag.tr(), ]; class Element { @@ -375,6 +385,19 @@ class Element { } } +function trimUnwantedSpaces(html) { + const body = html.match(/]*>([\s\S]*?)<\/body>/); + html = body ? body[1] : html; + html = html.replace(/\r|\n| /g, " "); + + let match; + while (match = html.match(/<[^\s>]+[^>]*>\s{2,}<[^\s>]+[^>]*>/)) { + html = html.replace(match[0], match[0].replace(/>\s{2,} <")); + } + + return html; +} + function putPlaceholders(html) { const codeRegEx = /]*>([\s\S]*?)<\/code>/gi; const origHtml = html; @@ -390,7 +413,7 @@ function putPlaceholders(html) { match = codeRegEx.exec(origHtml); } - const elements = parseHTML(html); + const elements = parseHTML(trimUnwantedSpaces(html)); return { elements, placeholders }; } @@ -406,7 +429,7 @@ export default function toMarkdown(html) { const { elements, placeholders } = putPlaceholders(html); let markdown = Element.parse(elements).trim(); markdown = markdown.replace(/^/, "").replace(/<\/b>$/, "").trim(); // fix for google doc copy paste - markdown = markdown.replace(/\r/g, "").replace(/\n \n/g, "\n\n").replace(/\n{3,}/g, "\n\n"); + markdown = markdown.replace(/ +\n/g, "\n").replace(/\n \n/g, "\n\n").replace(/\n{3,}/g, "\n\n"); return replacePlaceholders(markdown, placeholders); } catch(err) { return ""; diff --git a/test/javascripts/lib/to-markdown-test.js.es6 b/test/javascripts/lib/to-markdown-test.js.es6 index 0a33d642b9..5fbf9924cc 100644 --- a/test/javascripts/lib/to-markdown-test.js.es6 +++ b/test/javascripts/lib/to-markdown-test.js.es6 @@ -4,19 +4,21 @@ QUnit.module("lib:to-markdown"); QUnit.test("converts styles between normal words", assert => { const html = `Line with styles between words.`; - const markdown = `Line with ~~styles~~ **_between_** words.`; + const markdown = `Line with ~~styles~~ ***between*** words.`; assert.equal(toMarkdown(html), markdown); + + assert.equal(toMarkdown("A bold word"), "A **bold** word"); }); QUnit.test("converts inline nested styles", assert => { let html = `Italicised line with some random bold words.`; - let markdown = `_Italicised line with **some random** **bold** words._`; + let markdown = `*Italicised line with **some random** **bold** words.*`; assert.equal(toMarkdown(html), markdown); html = `Italicised line - with some + with some
random
bold words.
`; - markdown = `Italicised line\n with some\n random ~~bold~~ words.`; + markdown = `Italicised line with some\nrandom ~~bold~~ words.`; assert.equal(toMarkdown(html), markdown); }); @@ -26,7 +28,7 @@ QUnit.test("converts a link", assert => { assert.equal(toMarkdown(html), markdown); html = `Disc\n\n\nour\n\nse`; - markdown = `[Disc\nour\nse](https://discourse.org)`; + markdown = `[Disc our se](https://discourse.org)`; assert.equal(toMarkdown(html), markdown); }); @@ -82,7 +84,7 @@ QUnit.test("converts ul list tag", assert => {

  • Item 3
  • `; - const markdown = `* Item 1\n* Item 2\n\n * Sub Item 1\n * Sub Item 2\n\n * Sub _Sub_ Item 1\n * Sub **Sub** Item 2\n\n* Item 3`; + const markdown = `* Item 1\n* Item 2\n\n * Sub Item 1\n * Sub Item 2\n\n * Sub *Sub* Item 1\n * Sub **Sub** Item 2\n\n* Item 3`; assert.equal(toMarkdown(html), markdown); }); @@ -101,10 +103,12 @@ QUnit.test("converts table tags", assert => { Heading 1Head 2 Loremipsum - dolor sit amet + dolor sit amet + + `; - const markdown = `Discourse Avenue\n\n**laboris**\n\n|Heading 1|Head 2|\n| --- | --- |\n|Lorem|ipsum|\n|**dolor**|_sit amet_|`; + const markdown = `Discourse Avenue\n\n**laboris**\n\n|Heading 1|Head 2|\n| --- | --- |\n|Lorem|ipsum|\n|**dolor**|*sit amet*|`; assert.equal(toMarkdown(html), markdown); }); @@ -164,11 +168,11 @@ QUnit.test("supporting html tags by keeping them", assert => { output = `[Lorem ipsum dolor sit](http://example.com).`; assert.equal(toMarkdown(html), output); - html = `Lorem ipsum \n\n dolor sit.`; + html = `Lorem ipsum dolor sit.`; assert.equal(toMarkdown(html), html); html = `Lorem ipsum \n\n\n dolor sit.`; - output = `Lorem [ipsum \n dolor sit.](http://example.com)`; + output = `Lorem [ipsum dolor sit.](http://example.com)`; assert.equal(toMarkdown(html), output); }); @@ -223,6 +227,6 @@ QUnit.test("converts ol list tag", assert => {
  • Item 3
  • `; - const markdown = `Testing\n\n1. Item 1\n2. Item 2\n\n 100. Sub Item 1\n 101. Sub Item 2\n\n * Sub _Sub_ Item 1\n * Sub **Sub** Item 2\n\n3. Item 3`; + const markdown = `Testing\n\n1. Item 1\n2. Item 2\n\n 100. Sub Item 1\n 101. Sub Item 2\n\n * Sub *Sub* Item 1\n * Sub **Sub** Item 2\n\n3. Item 3`; assert.equal(toMarkdown(html), markdown); }); From 80eb492c075fe7e9590d722136ac4b864335ff42 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 22 Dec 2017 13:24:17 +0800 Subject: [PATCH 037/106] Add eslint and babel-eslint to yarn dev dependencies. --- package.json | 2 + yarn.lock | 738 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 734 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cb69e4f7bc..2668bc503a 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,10 @@ "license": "MIT", "dependencies": {}, "devDependencies": { + "babel-eslint": "^8.0.3", "chrome-launcher": "^0.10.0", "chrome-remote-interface": "^0.25.4", + "eslint": "^4.13.1", "puppeteer": "^0.13.0" } } diff --git a/yarn.lock b/yarn.lock index fba73f4af8..e73d58eb46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,59 @@ # yarn lockfile v1 +"@babel/code-frame@7.0.0-beta.31": + version "7.0.0-beta.31" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz#473d021ecc573a2cce1c07d5b509d5215f46ba35" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/helper-function-name@7.0.0-beta.31": + version "7.0.0-beta.31" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.31.tgz#afe63ad799209989348b1109b44feb66aa245f57" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.31" + "@babel/template" "7.0.0-beta.31" + "@babel/traverse" "7.0.0-beta.31" + "@babel/types" "7.0.0-beta.31" + +"@babel/helper-get-function-arity@7.0.0-beta.31": + version "7.0.0-beta.31" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.31.tgz#1176d79252741218e0aec872ada07efb2b37a493" + dependencies: + "@babel/types" "7.0.0-beta.31" + +"@babel/template@7.0.0-beta.31": + version "7.0.0-beta.31" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.31.tgz#577bb29389f6c497c3e7d014617e7d6713f68bda" + dependencies: + "@babel/code-frame" "7.0.0-beta.31" + "@babel/types" "7.0.0-beta.31" + babylon "7.0.0-beta.31" + lodash "^4.2.0" + +"@babel/traverse@7.0.0-beta.31": + version "7.0.0-beta.31" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.31.tgz#db399499ad74aefda014f0c10321ab255134b1df" + dependencies: + "@babel/code-frame" "7.0.0-beta.31" + "@babel/helper-function-name" "7.0.0-beta.31" + "@babel/types" "7.0.0-beta.31" + babylon "7.0.0-beta.31" + debug "^3.0.1" + globals "^10.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +"@babel/types@7.0.0-beta.31": + version "7.0.0-beta.31" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.31.tgz#42c9c86784f674c173fb21882ca9643334029de4" + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + "@types/core-js@^0.9.41": version "0.9.43" resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-0.9.43.tgz#65d646c5e8c0cd1bdee37065799f9d3d48748253" @@ -18,16 +71,106 @@ version "0.0.28" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-0.0.28.tgz#5562519bc7963caca8abf7f128cae3b594d41d06" +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" + agent-base@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.1.2.tgz#80fa6cde440f4dcf9af2617cf246099b5d99f0c8" dependencies: es6-promisify "^5.0.0" +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv@^5.2.3, ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ansi-escapes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" +babel-code-frame@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-eslint@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.3.tgz#f29ecf02336be438195325cd47c468da81ee4e98" + dependencies: + "@babel/code-frame" "7.0.0-beta.31" + "@babel/traverse" "7.0.0-beta.31" + "@babel/types" "7.0.0-beta.31" + babylon "7.0.0-beta.31" + +babylon@7.0.0-beta.31: + version "7.0.0-beta.31" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.31.tgz#7ec10f81e0e456fd0f855ad60fa30c2ac454283f" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -39,6 +182,38 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chardet@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" + chrome-launcher@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.10.0.tgz#4291ab1d20bb097da80b085e8c8c8e6b3e3d72c9" @@ -59,6 +234,34 @@ chrome-remote-interface@^0.25.4: commander "2.11.x" ws "3.3.x" +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + commander@2.11.x: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -67,7 +270,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@1.6.0: +concat-stream@1.6.0, concat-stream@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -79,18 +282,48 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + debug@2.6.9, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: ms "2.0.0" -debug@^3.1.0: +debug@^3.0.1, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: ms "2.0.0" +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +doctrine@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.2.tgz#68f96ce8efc56cc42651f1faadb4f175273b0075" + dependencies: + esutils "^2.0.2" + es6-promise@^4.0.3: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" @@ -101,6 +334,99 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +eslint-scope@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint@^4.13.1: + version "4.13.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.13.1.tgz#0055e0014464c7eb7878caf549ef2941992b444f" + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.0.1" + doctrine "^2.0.2" + eslint-scope "^3.7.1" + espree "^3.5.2" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "^4.0.1" + text-table "~0.2.0" + +espree@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" + dependencies: + acorn "^5.2.1" + acorn-jsx "^3.0.0" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esquery@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +external-editor@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48" + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + extract-zip@^1.6.5: version "1.6.6" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" @@ -110,17 +436,55 @@ extract-zip@^1.6.5: mkdirp "0.5.0" yauzl "2.4.1" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + fd-slicer@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" dependencies: pend "~1.2.0" +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +flat-cache@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -glob@^7.0.5: +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -131,6 +495,39 @@ glob@^7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" +globals@^10.0.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-10.4.0.tgz#5c477388b128a9e4c5c5d01c7a2aca68c68b2da7" + +globals@^11.0.1: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + https-proxy-agent@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.1.1.tgz#a7ce4382a1ba8266ee848578778122d491260fd9" @@ -138,6 +535,18 @@ https-proxy-agent@^2.1.0: agent-base "^4.1.0" debug "^3.1.0" +iconv-lite@^0.4.17: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +ignore@^3.3.3: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -149,6 +558,59 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" +inquirer@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +invariant@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-resolvable@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.1.tgz#acca1cd36dbe44b974b924321555a70ba03b1cf4" + is-wsl@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" @@ -157,17 +619,68 @@ isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.9.1: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + lighthouse-logger@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.0.1.tgz#f073d83f7acbc96729bf100a121c8f006991ae61" dependencies: debug "^2.6.8" +lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" -minimatch@^3.0.4: +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -183,7 +696,7 @@ mkdirp@0.5.0: dependencies: minimist "0.0.8" -mkdirp@0.5.1: +mkdirp@0.5.1, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -193,20 +706,79 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: wrappy "1" +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -219,6 +791,10 @@ proxy-from-env@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + puppeteer@^0.13.0: version "0.13.0" resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-0.13.0.tgz#2e6956205f2c640964c2107f620ae1eef8bde8fd" @@ -244,22 +820,152 @@ readable-stream@^2.2.2: string_decoder "~1.0.3" util-deprecate "~1.0.1" -rimraf@^2.6.1: +require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +rimraf@^2.2.8, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +semver@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + dependencies: + is-fullwidth-code-point "^2.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: safe-buffer "~5.1.0" +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +table@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" + dependencies: + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -272,10 +978,26 @@ util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + ws@3.3.x, ws@^3.0.0: version "3.3.2" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.2.tgz#96c1d08b3fefda1d5c1e33700d3bfaa9be2d5608" @@ -284,6 +1006,10 @@ ws@3.3.x, ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" From d80aca048424e43ada26ee097c63c4171683b6c6 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 22 Dec 2017 15:46:46 +0800 Subject: [PATCH 038/106] FIX: Don't start discobot poll tutorial if polls are disabled. https://meta.discourse.org/t/discobot-dont-take-users-through-the-poll-tutorial-if-its-not-enabled/76604/3 --- .../advanced_user_narrative.rb | 1 + .../advanced_user_narrative_spec.rb | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb index b5fbe5bc73..a503392f95 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb @@ -80,6 +80,7 @@ module DiscourseNarrativeBot }, tutorial_poll: { + prerequisite: Proc.new { SiteSetting.poll_enabled }, next_state: :tutorial_details, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.details.instructions", i18n_post_args) }, reply: { diff --git a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb index c7ae682061..81f0e01155 100644 --- a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb +++ b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb @@ -542,6 +542,29 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_poll) end end + + describe 'when poll is disabled' do + before do + SiteSetting.poll_enabled = false + end + + it 'should create the right reply' do + TopicUser.change( + user.id, + topic.id, + notification_level: TopicUser.notification_levels[:tracking] + ) + + expected_raw = <<~RAW + #{I18n.t('discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.reply', base_uri: '')} + + #{I18n.t('discourse_narrative_bot.advanced_user_narrative.details.instructions', base_uri: '')} + RAW + + expect(Post.last.raw).to eq(expected_raw.chomp) + expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_details) + end + end end context 'poll tutorial' do From 364e6fdd53fcdd1af2a5e09de5600565ad3a28da Mon Sep 17 00:00:00 2001 From: blokovi Date: Fri, 22 Dec 2017 12:20:19 +0100 Subject: [PATCH 039/106] FIX: pluralization rules for Serbian language (#5453) Updated SR pluralization to use 3 keys: one, few, other (as by Transifex) --- config/locales/plurals.rb | 2 +- lib/javascripts/locale/sr.js | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/config/locales/plurals.rb b/config/locales/plurals.rb index ad223446ff..f4d4acc038 100644 --- a/config/locales/plurals.rb +++ b/config/locales/plurals.rb @@ -92,7 +92,7 @@ sms: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, so: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, sq: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, - sr: { i18n: { plural: { keys: [:one, :few, :many, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, + sr: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : :other } } } }, sv: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, sw: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, ta: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, diff --git a/lib/javascripts/locale/sr.js b/lib/javascripts/locale/sr.js index c2e3d3df1c..9bd8663452 100644 --- a/lib/javascripts/locale/sr.js +++ b/lib/javascripts/locale/sr.js @@ -1,14 +1,11 @@ MessageFormat.locale.sr = function (n) { - if ((n % 10) == 1 && (n % 100) != 11) { + var r10 = n % 10, r100 = n % 100; + + if (r10 == 1 && r100 != 11) return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 4 && - ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { + + if (r10 >= 2 && r10 <= 4 && (r100 < 12 || r100 > 14) && n == Math.floor(n)) return 'few'; - } - if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || - ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { - return 'many'; - } + return 'other'; -}; +}; \ No newline at end of file From 315b9d796dd5f1e1095fd8ce2dd6b3a9866eff43 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 22 Dec 2017 13:08:12 +0100 Subject: [PATCH 040/106] select-kit refactoring * better test helper * more reliable tests * more consistent use of data-value/data-name/title/aria-label everywhere: header and rows --- .eslintrc | 5 +- .../discourse/controllers/composer.js.es6 | 4 + .../components/edit-category-security.hbs | 2 +- .../templates/preferences/notifications.hbs | 2 +- .../components/category-selector.js.es6 | 5 + .../dropdown-select-box-row.js.es6 | 1 - .../select-kit/components/multi-select.js.es6 | 1 + .../multi-select/multi-select-header.js.es6 | 22 +- .../multi-select/selected-category.js.es6 | 5 +- .../multi-select/selected-color.js.es6 | 2 +- .../multi-select/selected-name.js.es6 | 30 ++- .../components/pinned-options.js.es6 | 5 +- .../select-kit/components/select-kit.js.es6 | 4 +- .../select-kit/select-kit-header.js.es6 | 27 ++- .../select-kit/select-kit-row.js.es6 | 28 ++- .../components/single-select.js.es6 | 2 + .../templates/components/category-row.hbs | 2 +- .../components/combo-box/combo-box-header.hbs | 4 +- .../dropdown-select-box-header.hbs | 2 +- .../dropdown-select-box-row.hbs | 2 +- .../future-date-input-selector-header.hbs | 2 +- .../future-date-input-selector-row.hbs | 2 +- .../multi-select/multi-select-header.hbs | 2 +- .../multi-select/selected-category.hbs | 2 +- .../multi-select/selected-color.hbs | 4 +- .../components/multi-select/selected-name.hbs | 4 +- .../templates/components/select-kit.hbs | 1 - .../select-kit/select-kit-collection.hbs | 4 +- .../select-kit/select-kit-header.hbs | 4 +- .../components/select-kit/select-kit-row.hbs | 2 +- .../acceptance/details-button-test.js.es6 | 27 ++- .../poll-builder-disabled-test.js.es6 | 4 + .../poll-builder-enabled-test.js.es6 | 4 + .../javascripts/acceptance/polls-test.js.es6 | 6 +- .../display-poll-builder-button.js.es6 | 3 +- .../acceptance/admin-flags-test.js.es6 | 60 ++---- .../acceptance/admin-suspend-user-test.js.es6 | 9 +- .../acceptance/category-chooser-test.js.es6 | 20 +- .../category-edit-security-test.js.es6 | 26 +-- .../acceptance/category-edit-test.js.es6 | 10 +- .../acceptance/composer-test.js.es6 | 7 +- .../acceptance/preferences-test.js.es6 | 4 +- .../acceptance/search-full-test.js.es6 | 36 ++-- .../javascripts/acceptance/search-test.js.es6 | 48 +++-- .../acceptance/topic-edit-timer-test.js.es6 | 117 +++++++---- .../topic-notifications-button-test.js.es6 | 20 +- test/javascripts/acceptance/topic-test.js.es6 | 11 +- .../categories-admin-dropdown-test.js.es6 | 10 +- .../components/category-chooser-test.js.es6 | 62 +++--- .../components/category-selector-test.js.es6 | 87 ++++++++ .../components/categpry-selector-test.js.es6 | 67 ------ .../components/combo-box-test.js.es6 | 125 ++++------- .../components/list-setting-test.js.es6 | 35 ++-- .../components/multi-select-test.js.es6 | 96 ++++++--- .../components/pinned-options-test.js.es6 | 15 +- .../components/single-select-test.js.es6 | 197 +++++++++++------- .../topic-footer-mobile-dropdown-test.js.es6 | 27 ++- .../topic-notifications-button-test.js.es6 | 4 +- test/javascripts/helpers/assertions.js | 6 - test/javascripts/helpers/select-kit-helper.js | 143 ++++++++----- 60 files changed, 827 insertions(+), 641 deletions(-) create mode 100644 test/javascripts/components/category-selector-test.js.es6 delete mode 100644 test/javascripts/components/categpry-selector-test.js.es6 diff --git a/.eslintrc b/.eslintrc index bfbe34ea65..218cb72b81 100644 --- a/.eslintrc +++ b/.eslintrc @@ -41,11 +41,12 @@ "visible":true, "invisible":true, "asyncRender":true, - "selectDropdown":true, "selectKit":true, "expandSelectKit":true, "collapseSelectKit":true, - "selectKitSelectRow":true, + "selectKitSelectRowByValue":true, + "selectKitSelectRowByName":true, + "selectKitSelectRowByIndex":true, "selectKitSelectNoneRow":true, "selectKitFillInFilter":true, "asyncTestDiscourse":true, diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 0e6f9c6193..35f6b256e4 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -49,6 +49,10 @@ function loadDraft(store, opts) { const _popupMenuOptionsCallbacks = []; +export function clearPopupMenuOptionsCallback() { + _popupMenuOptionsCallbacks.length = 0; +} + export function addPopupMenuOptionsCallback(callback) { _popupMenuOptionsCallbacks.push(callback); } diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs index fb0a13c213..ad078cda2f 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs @@ -18,7 +18,7 @@ {{#if category.availableGroups}} {{combo-box class="available-groups" allowInitialValueMutation=true - allowsContentReplacement=true + allowContentReplacement=true content=category.availableGroups value=selectedGroup}} {{combo-box allowInitialValueMutation=true diff --git a/app/assets/javascripts/discourse/templates/preferences/notifications.hbs b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs index 987a9c1386..c0441202b4 100644 --- a/app/assets/javascripts/discourse/templates/preferences/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs @@ -1,7 +1,7 @@
    - {{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.user_option.new_topic_duration_minutes}} + {{combo-box class="duration" valueAttribute="value" content=considerNewTopicOptions value=model.user_option.new_topic_duration_minutes}}
    diff --git a/app/assets/javascripts/select-kit/components/category-selector.js.es6 b/app/assets/javascripts/select-kit/components/category-selector.js.es6 index 0c31747cbc..9484e13844 100644 --- a/app/assets/javascripts/select-kit/components/category-selector.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-selector.js.es6 @@ -7,10 +7,15 @@ export default MultiSelectComponent.extend({ filterable: true, allowAny: false, rowComponent: "category-row", + categories: null, + blacklist: null, init() { this._super(); + if (!this.get("categories")) this.set("categories", []); + if (!this.get("blacklist")) this.set("blacklist", []); + this.get("headerComponentOptions").setProperties({ selectedNameComponent: "multi-select/selected-category" }); diff --git a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 index fd865fb602..0d5989717c 100644 --- a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 @@ -4,6 +4,5 @@ export default SelectKitRowComponent.extend({ layoutName: "select-kit/templates/components/dropdown-select-box/dropdown-select-box-row", classNames: "dropdown-select-box-row", - name: Ember.computed.alias("computedContent.name"), description: Ember.computed.alias("computedContent.originalContent.description") }); diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6 index 9358d75e61..2929584f81 100644 --- a/app/assets/javascripts/select-kit/components/multi-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6 @@ -116,6 +116,7 @@ export default SelectKitComponent.extend({ baseHeaderComputedContent() { return { + title: this.get("title"), selectedComputedContents: this.get("selectedComputedContents") }; }, diff --git a/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 index d86af77ed8..2db2019689 100644 --- a/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 @@ -3,14 +3,23 @@ import computed from "ember-addons/ember-computed-decorators"; import SelectKitHeaderComponent from "select-kit/components/select-kit/select-kit-header"; export default SelectKitHeaderComponent.extend({ - attributeBindings: ["names:data-name"], + attributeBindings: [ + "label:title", + "label:aria-label", + "names:data-name", + "values:data-value" + ], classNames: "multi-select-header", layoutName: "select-kit/templates/components/multi-select/multi-select-header", selectedNameComponent: Ember.computed.alias("options.selectedNameComponent"), + ariaLabel: Ember.computed.or("computedContent.ariaLabel", "title", "names"), + + title: Ember.computed.or("computedContent.title", "names"), + @on("didRender") _positionFilter() { - if (this.get("shouldDisplayFilter") === false) { return; } + if (!this.get("shouldDisplayFilter")) return; const $filter = this.$(".filter"); $filter.width(0); @@ -26,7 +35,12 @@ export default SelectKitHeaderComponent.extend({ }, @computed("computedContent.selectedComputedContents.[]") - names(selectedComputedContents) { - return Ember.makeArray(selectedComputedContents).map(sc => sc.name).join(","); + names(selection) { + return Ember.makeArray(selection).map(s => s.name).join(","); + }, + + @computed("computedContent.selectedComputedContents.[]") + values(selection) { + return Ember.makeArray(selection).map(s => s.value).join(","); } }); diff --git a/app/assets/javascripts/select-kit/components/multi-select/selected-category.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/selected-category.js.es6 index 0a09d69fab..200f6a747a 100644 --- a/app/assets/javascripts/select-kit/components/multi-select/selected-category.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select/selected-category.js.es6 @@ -6,8 +6,9 @@ export default SelectedNameComponent.extend({ classNames: "selected-category", layoutName: "select-kit/templates/components/multi-select/selected-category", - @computed("content.originalContent") + @computed("computedContent.originalContent") badge(category) { - return categoryBadgeHTML(category, {allowUncategorized: true, link: false}).htmlSafe(); + return categoryBadgeHTML(category, { allowUncategorized: true, link: false }) + .htmlSafe(); } }); diff --git a/app/assets/javascripts/select-kit/components/multi-select/selected-color.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/selected-color.js.es6 index 3349d95ff8..7e835e8439 100644 --- a/app/assets/javascripts/select-kit/components/multi-select/selected-color.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select/selected-color.js.es6 @@ -5,7 +5,7 @@ export default SelectedNameComponent.extend({ layoutName: "select-kit/templates/components/multi-select/selected-color", didRender() { - const name = this.get("content.name"); + const name = this.get("name"); this.$(".color-preview").css("background", `#${name}`.htmlSafe()); } }); diff --git a/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 index a201791c03..a6b672d50e 100644 --- a/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 @@ -3,8 +3,10 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ attributeBindings: [ "tabindex", - "content.name:data-name", - "content.value:data-value", + "ariaLabel:aria-label", + "title", + "name:data-name", + "value:data-value", "guid:data-guid" ], classNames: ["selected-name", "choice"], @@ -13,11 +15,27 @@ export default Ember.Component.extend({ tagName: "span", tabindex: -1, - @computed("content") - guid(content) { return Ember.guidFor(content); }, + @computed("computedContent") + guid(computedContent) { return Ember.guidFor(computedContent); }, - isLocked: Ember.computed("content.locked", function() { - return this.getWithDefault("content.locked", false); + ariaLabel: Ember.computed.or("computedContent.ariaLabel", "title"), + + @computed("computedContent.title", "name") + title(computedContentTitle, name) { + if (computedContentTitle) return computedContentTitle; + if (name) return name; + + return null; + }, + + label: Ember.computed.or("computedContent.label", "title", "name"), + + name: Ember.computed.alias("computedContent.name"), + + value: Ember.computed.alias("computedContent.value"), + + isLocked: Ember.computed("computedContent.locked", function() { + return this.getWithDefault("computedContent.locked", false); }), click() { diff --git a/app/assets/javascripts/select-kit/components/pinned-options.js.es6 b/app/assets/javascripts/select-kit/components/pinned-options.js.es6 index 4d1aefb9d0..ca4c65289c 100644 --- a/app/assets/javascripts/select-kit/components/pinned-options.js.es6 +++ b/app/assets/javascripts/select-kit/components/pinned-options.js.es6 @@ -17,8 +17,9 @@ export default DropdownSelectBoxComponent.extend({ const state = pinned ? `pinned${globally}` : "unpinned"; const title = I18n.t(`topic_statuses.${state}.title`); - content.name = `${title}${iconHTML("caret-down")}`.htmlSafe(); - content.dataName = title; + content.label = `${title}${iconHTML("caret-down")}`.htmlSafe(); + content.title = title; + content.name = state; content.icon = `thumb-tack${state === "unpinned" ? " unpinned" : ''}`; return content; }, diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6 index c34c8b10f8..90cb7c9d99 100644 --- a/app/assets/javascripts/select-kit/components/select-kit.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6 @@ -61,7 +61,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi computedContent: null, limitMatches: 100, nameChanges: false, - allowsContentReplacement: false, + allowContentReplacement: false, collectionHeader: null, init() { @@ -82,7 +82,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi this.addObserver(`content.@each.${this.get("nameProperty")}`, this, this._compute); } - if (this.get("allowsContentReplacement")) { + if (this.get("allowContentReplacement")) { this.addObserver(`content.[]`, this, this._compute); } }, diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 index e827709568..b7705fa999 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 @@ -5,32 +5,37 @@ export default Ember.Component.extend({ classNames: ["select-kit-header", "select-box-kit-header"], classNameBindings: ["isFocused"], attributeBindings: [ - "dataName:data-name", "tabindex", "ariaLabel:aria-label", "ariaHasPopup:aria-haspopup", - "title" + "title", + "value:data-value", + "name:data-name", ], ariaHasPopup: true, - ariaLabel: Ember.computed.alias("title"), + ariaLabel: Ember.computed.or("computedContent.ariaLabel", "title"), + + @computed("computedContent.title", "name") + title(computedContentTitle, name) { + if (computedContentTitle) return computedContentTitle; + if (name) return name; + + return null; + }, + + label: Ember.computed.or("computedContent.label", "title", "name"), name: Ember.computed.alias("computedContent.name"), + value: Ember.computed.alias("computedContent.value"), + @computed("computedContent.icon", "computedContent.icons") icons(icon, icons) { return Ember.makeArray(icon).concat(icons).filter(i => !Ember.isEmpty(i)); }, - @computed("computedContent.dataName", "name") - dataName(dataName, name) { return dataName || name; }, - - @computed("title", "computedContent.title", "name") - title(title, computedContentTitle, name) { - return title || computedContentTitle || name; - }, - click() { this.sendAction("onToggle"); } diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 index f9c46b0a1e..d4638a9386 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 @@ -11,23 +11,35 @@ export default Ember.Component.extend(UtilsMixin, { attributeBindings: [ "tabIndex", "title", - "computedContent.value:data-value", - "computedContent.name:data-name" + "value:data-value", + "name:data-name", + "ariaLabel:aria-label" ], classNameBindings: ["isHighlighted", "isSelected"], - @computed("computedContent.title", "computedContent.name") - title(title, name) { return title || name; }, + ariaLabel: Ember.computed.or("computedContent.ariaLabel", "title"), + + @computed("computedContent.title", "name") + title(computedContentTitle, name) { + if (computedContentTitle) return computedContentTitle; + if (name) return name; + + return null; + }, + + label: Ember.computed.or("computedContent.label", "title", "name"), + + name: Ember.computed.alias("computedContent.name"), + + value: Ember.computed.alias("computedContent.value"), @computed("templateForRow") template(templateForRow) { return templateForRow(this); }, @on("didReceiveAttrs") _setSelectionState() { - const contentValue = this.get("computedContent.value"); - - this.set("isSelected", this.get("computedValue") === contentValue); - this.set("isHighlighted", this.get("highlightedValue") === contentValue); + this.set("isSelected", this.get("computedValue") === this.get("value")); + this.set("isHighlighted", this.get("highlightedValue") === this.get("value")); }, @on("willDestroyElement") diff --git a/app/assets/javascripts/select-kit/components/single-select.js.es6 b/app/assets/javascripts/select-kit/components/single-select.js.es6 index c05c6b7db0..8e2a35a5ef 100644 --- a/app/assets/javascripts/select-kit/components/single-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/single-select.js.es6 @@ -78,7 +78,9 @@ export default SelectKitComponent.extend({ baseHeaderComputedContent() { return { + title: this.get("title"), icons: Ember.makeArray(this.getWithDefault("headerIcon", [])), + value: this.get("selectedComputedContent.value"), name: this.get("selectedComputedContent.name") || this.get("noneRowComputedContent.name") }; }, diff --git a/app/assets/javascripts/select-kit/templates/components/category-row.hbs b/app/assets/javascripts/select-kit/templates/components/category-row.hbs index 0f3249ea46..6ea0bf428c 100644 --- a/app/assets/javascripts/select-kit/templates/components/category-row.hbs +++ b/app/assets/javascripts/select-kit/templates/components/category-row.hbs @@ -15,5 +15,5 @@
    {{{description}}}
    {{/if}} {{else}} - {{computedContent.name}} + {{{label}}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs index adc9440ce5..97a66905a9 100644 --- a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs @@ -1,7 +1,7 @@ {{#each icons as |icon|}} {{d-icon icon}} {{/each}} - - {{{name}}} + + {{{label}}} {{#if shouldDisplayClearableButton}} diff --git a/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs index 9cc8c31cf6..48af8fab95 100644 --- a/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs @@ -2,6 +2,6 @@ {{#if options.showFullTitle}} - {{name}} + {{label}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs index 556551a61f..d6884bd75a 100644 --- a/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs +++ b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs @@ -9,7 +9,7 @@ {{/if}}
    - {{{name}}} + {{{label}}} {{{description}}}
    {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs index 4c8904e9a3..caa167199e 100644 --- a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs @@ -5,7 +5,7 @@ {{/if}} - {{{name}}} + {{{label}}} {{#if computedContent.datetime}} diff --git a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs index 8d62c2d1f9..a0217afca1 100644 --- a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs +++ b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs @@ -4,7 +4,7 @@
    {{/if}} -{{computedContent.name}} +{{label}} {{#if computedContent.datetime}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs index 466b5c6d25..5cfd580279 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs @@ -1,6 +1,6 @@
    {{#each computedContent.selectedComputedContents as |selectedComputedContent|}} - {{component selectedNameComponent onDeselect=onDeselect content=selectedComputedContent}} + {{component selectedNameComponent onDeselect=onDeselect computedContent=selectedComputedContent}} {{/each}} {{component "select-kit/select-kit-filter" diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-category.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-category.hbs index c220d68cfc..d96b31245b 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-category.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-category.hbs @@ -1,5 +1,5 @@ - + {{d-icon "times"}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-color.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-color.hbs index c0d544ab9d..2d70a3d7df 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-color.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-color.hbs @@ -1,12 +1,12 @@
    {{#unless isLocked}} - + {{d-icon "times"}} {{/unless}} - #{{content.name}} + #{{{label}}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs index 721a30ef21..a48a160ccc 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs @@ -3,11 +3,11 @@ {{d-icon "lock"}} {{else}} - + {{d-icon "times"}} {{/if}} - {{content.name}} + {{{label}}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit.hbs index adc84ca1e7..576cab2248 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit.hbs @@ -9,7 +9,6 @@ onClear=(action "onClear") options=headerComponentOptions shouldDisplayFilter=shouldDisplayFilter - title=(i18n title) }}
    diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs index ca48220bb6..f6cd9cf1a6 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs @@ -12,7 +12,7 @@ highlightedValue=highlightedValue onClear=onClear onHighlight=onHighlight - value=computedValue + computedValue=computedValue options=rowComponentOptions }} {{/if}} @@ -25,7 +25,7 @@ highlightedValue=highlightedValue onHighlight=onHighlight onCreate=onCreate - value=computedValue + computedValue=computedValue options=rowComponentOptions }} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs index abd119afda..e65ce13500 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs @@ -1,5 +1,5 @@ {{#each icons as |icon|}} {{d-icon icon}} {{/each}} - - {{{name}}} + + {{{label}}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs index 80444173f4..fc0df65835 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs @@ -2,5 +2,5 @@ {{{template}}} {{else}} {{#each icons as |icon|}} {{d-icon icon}} {{/each}} - {{computedContent.name}} + {{{label}}} {{/if}} 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 79bbeddcdf..a22705223e 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 @@ -1,16 +1,24 @@ import { acceptance } from "helpers/qunit-helpers"; +import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer"; -acceptance('Details Button', { loggedIn: true }); +acceptance('Details Button', { + loggedIn: true, + beforeEach: function() { + clearPopupMenuOptionsCallback(); + } +}); function findTextarea() { return find(".d-editor-input")[0]; } test('details button', (assert) => { + const popupMenu = selectKit('.toolbar-popup-menu-options'); + visit("/"); click('#create-topic'); - expandSelectKit('.toolbar-popup-menu-options'); - selectKitSelectRow('insertDetails', { selector: '.toolbar-popup-menu-options'}); + + popupMenu.expand().selectRowByValue('insertDetails'); andThen(() => { assert.equal( @@ -28,8 +36,7 @@ test('details button', (assert) => { textarea.selectionEnd = textarea.value.length; }); - expandSelectKit('.toolbar-popup-menu-options'); - selectKitSelectRow('insertDetails', { selector: '.toolbar-popup-menu-options'}); + popupMenu.expand().selectRowByValue('insertDetails'); andThen(() => { assert.equal( @@ -51,8 +58,7 @@ test('details button', (assert) => { textarea.selectionEnd = 28; }); - expandSelectKit('.toolbar-popup-menu-options'); - selectKitSelectRow('insertDetails', { selector: '.toolbar-popup-menu-options'}); + popupMenu.expand().selectRowByValue('insertDetails'); andThen(() => { assert.equal( @@ -74,8 +80,7 @@ test('details button', (assert) => { textarea.selectionEnd = 29; }); - expandSelectKit('.toolbar-popup-menu-options'); - selectKitSelectRow('insertDetails', { selector: '.toolbar-popup-menu-options'}); + popupMenu.expand().selectRowByValue('insertDetails'); andThen(() => { assert.equal( @@ -92,6 +97,7 @@ test('details button', (assert) => { test('details button surrounds all selected text in a single details block', (assert) => { const multilineInput = 'first line\n\nsecond line\n\nthird line'; + const popupMenu = selectKit('.toolbar-popup-menu-options'); visit("/"); click('#create-topic'); @@ -103,8 +109,7 @@ test('details button surrounds all selected text in a single details block', (as textarea.selectionEnd = textarea.value.length; }); - expandSelectKit('.toolbar-popup-menu-options'); - selectKitSelectRow('insertDetails', { selector: '.toolbar-popup-menu-options'}); + popupMenu.expand().selectRowByValue('insertDetails'); andThen(() => { assert.equal( diff --git a/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 index 4fef0109dd..69f9fb4365 100644 --- a/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 @@ -1,12 +1,16 @@ import { acceptance } from "helpers/qunit-helpers"; import { displayPollBuilderButton } from "discourse/plugins/poll/helpers/display-poll-builder-button"; import { replaceCurrentUser } from "discourse/plugins/poll/helpers/replace-current-user"; +import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer"; acceptance("Poll Builder - polls are disabled", { loggedIn: true, settings: { poll_enabled: false, poll_minimum_trust_level_to_create: 2 + }, + beforeEach: function() { + clearPopupMenuOptionsCallback(); } }); diff --git a/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 index 9d0eef23fa..4f210446f4 100644 --- a/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 @@ -1,12 +1,16 @@ import { acceptance } from "helpers/qunit-helpers"; import { displayPollBuilderButton } from "discourse/plugins/poll/helpers/display-poll-builder-button"; import { replaceCurrentUser } from "discourse/plugins/poll/helpers/replace-current-user"; +import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer"; acceptance("Poll Builder - polls are enabled", { loggedIn: true, settings: { poll_enabled: true, poll_minimum_trust_level_to_create: 1 + }, + beforeEach: function() { + clearPopupMenuOptionsCallback(); } }); diff --git a/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 index c6cac9d19a..b6f81eba61 100644 --- a/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 @@ -1,8 +1,12 @@ import { acceptance } from "helpers/qunit-helpers"; +import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer"; acceptance("Rendering polls", { loggedIn: true, - settings: { poll_enabled: true } + settings: { poll_enabled: true }, + beforeEach: function() { + clearPopupMenuOptionsCallback(); + } }); test("Single Poll", (assert) => { diff --git a/plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 b/plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 index 6ee6fa21cd..ba6bdccdfb 100644 --- a/plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 +++ b/plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 @@ -2,6 +2,5 @@ export function displayPollBuilderButton() { visit("/"); click("#create-topic"); click(".d-editor-button-bar .options"); - - expandSelectKit('.toolbar-popup-menu-options'); + selectKit(".toolbar-popup-menu-options").expand(); } diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index 3b25ee2f08..e64e823039 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -13,15 +13,11 @@ QUnit.test("flagged posts", assert => { }); QUnit.test("flagged posts - agree", assert => { + const agreeFlag = selectKit('.agree-flag'); + visit("/admin/flags/active"); - andThen(() => { - expandSelectKit('.agree-flag'); - }); - - andThen(() => { - selectKitSelectRow('confirm-agree-keep', { selector: '.agree-flag'}); - }); + agreeFlag.expand().selectRowByValue('confirm-agree-keep'); andThen(() => { assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); @@ -29,15 +25,11 @@ QUnit.test("flagged posts - agree", assert => { }); QUnit.test("flagged posts - agree + hide", assert => { + const agreeFlag = selectKit('.agree-flag'); + visit("/admin/flags/active"); - andThen(() => { - expandSelectKit('.agree-flag'); - }); - - andThen(() => { - selectKitSelectRow('confirm-agree-hide', { selector: '.agree-flag'}); - }); + agreeFlag.expand().selectRowByValue('confirm-agree-hide'); andThen(() => { assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); @@ -45,15 +37,11 @@ QUnit.test("flagged posts - agree + hide", assert => { }); QUnit.test("flagged posts - agree + deleteSpammer", assert => { + const agreeFlag = selectKit('.agree-flag'); + visit("/admin/flags/active"); - andThen(() => { - expandSelectKit('.agree-flag'); - }); - - andThen(() => { - selectKitSelectRow('delete-spammer', { selector: '.agree-flag'}); - }); + agreeFlag.expand().selectRowByValue('delete-spammer'); click('.confirm-delete'); @@ -79,15 +67,11 @@ QUnit.test("flagged posts - defer", assert => { }); QUnit.test("flagged posts - delete + defer", assert => { + const deleteFlag = selectKit('.delete-flag'); + visit("/admin/flags/active"); - andThen(() => { - expandSelectKit('.delete-flag'); - }); - - andThen(() => { - selectKitSelectRow('delete-defer', { selector: '.delete-flag'}); - }); + deleteFlag.expand().selectRowByValue('delete-defer'); andThen(() => { assert.equal(find('.admin-flags .flagged-post').length, 0); @@ -95,15 +79,11 @@ QUnit.test("flagged posts - delete + defer", assert => { }); QUnit.test("flagged posts - delete + agree", assert => { + const deleteFlag = selectKit('.delete-flag'); + visit("/admin/flags/active"); - andThen(() => { - expandSelectKit('.delete-flag'); - }); - - andThen(() => { - selectKitSelectRow('delete-agree', { selector: '.delete-flag'}); - }); + deleteFlag.expand().selectRowByValue('delete-agree'); andThen(() => { assert.equal(find('.admin-flags .flagged-post').length, 0); @@ -111,15 +91,11 @@ QUnit.test("flagged posts - delete + agree", assert => { }); QUnit.test("flagged posts - delete + deleteSpammer", assert => { + const deleteFlag = selectKit('.delete-flag'); + visit("/admin/flags/active"); - andThen(() => { - expandSelectKit('.delete-flag'); - }); - - andThen(() => { - selectKitSelectRow('delete-spammer', { selector: '.delete-flag'}); - }); + deleteFlag.expand().selectRowByValue('delete-spammer'); 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 cfe7320601..cc67726886 100644 --- a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 +++ b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 @@ -33,6 +33,10 @@ QUnit.test("suspend a user - cancel", assert => { }); QUnit.test("suspend, then unsuspend a user", assert => { + const suspendUntilCombobox = selectKit('.suspend-until .combobox'); + + visit("/admin/flags/active"); + visit("/admin/users/1234/regular"); andThen(() => { @@ -43,11 +47,10 @@ QUnit.test("suspend, then unsuspend a user", assert => { andThen(() => { assert.equal(find('.perform-suspend[disabled]').length, 1, 'disabled by default'); - - expandSelectKit('.suspend-until .combobox'); - selectKitSelectRow('tomorrow', { selector: '.suspend-until .combobox'}); }); + suspendUntilCombobox.expand().selectRowByValue('tomorrow'); + fillIn('.suspend-reason', "for breaking the rules"); fillIn('.suspend-message', "this is an email reason why"); andThen(() => { diff --git a/test/javascripts/acceptance/category-chooser-test.js.es6 b/test/javascripts/acceptance/category-chooser-test.js.es6 index 793674295f..bd3979993f 100644 --- a/test/javascripts/acceptance/category-chooser-test.js.es6 +++ b/test/javascripts/acceptance/category-chooser-test.js.es6 @@ -1,27 +1,29 @@ -import { acceptance } from "helpers/qunit-helpers"; +import { acceptance } from 'helpers/qunit-helpers'; -acceptance("CategoryChooser", { +acceptance('CategoryChooser', { loggedIn: true, settings: { allow_uncategorized_topics: false } }); -QUnit.test("does not display uncategorized if not allowed", assert => { - visit("/"); +QUnit.test('does not display uncategorized if not allowed', assert => { + const categoryChooser = selectKit('.category-chooser'); + + visit('/'); click('#create-topic'); - expandSelectKit('.category-chooser'); + categoryChooser.expand(); andThen(() => { - assert.ok(selectKit('.category-chooser').rowByIndex(0).name() !== 'uncategorized'); + assert.ok(categoryChooser.rowByIndex(0).name() !== 'uncategorized'); }); }); -QUnit.test("prefill category when category_id is set", assert => { - visit("/new-topic?category_id=1"); +QUnit.test('prefill category when category_id is set', assert => { + visit('/new-topic?category_id=1'); andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), 'bug'); + assert.equal(selectKit('.category-chooser').header().value(), 1); }); }); diff --git a/test/javascripts/acceptance/category-edit-security-test.js.es6 b/test/javascripts/acceptance/category-edit-security-test.js.es6 index 9d0b6c7129..2af84a1898 100644 --- a/test/javascripts/acceptance/category-edit-security-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-security-test.js.es6 @@ -22,35 +22,38 @@ QUnit.test("default", assert => { }); QUnit.test("removing a permission", assert => { + const availableGroups = selectKit(".available-groups"); + visit("/c/bug"); click('.edit-category'); click('li.edit-category-security a'); click('.edit-category-tab-security .edit-permission'); - expandSelectKit(".available-groups"); + availableGroups.expand(); andThen(() => { - assert.notOk(exists(".available-groups .select-kit-row[data-value='everyone']"), 'everyone is already used and is not in the available groups'); + assert.notOk(availableGroups.rowByValue('everyone').exists(), 'everyone is already used and is not in the available groups'); }); click('.edit-category-tab-security .permission-list li:first-of-type .remove-permission'); - expandSelectKit(".available-groups"); + availableGroups.expand(); andThen(() => { - assert.ok(exists(".available-groups .select-kit-row[data-value='everyone']"), 'everyone has been removed and appears in the available groups'); + assert.ok(availableGroups.rowByValue('everyone').exists(), 'everyone has been removed and appears in the available groups'); }); }); QUnit.test("adding a permission", assert => { + const availableGroups = selectKit(".available-groups"); + const permissionSelector = selectKit(".permission-selector"); + visit("/c/bug"); click('.edit-category'); click('li.edit-category-security a'); click('.edit-category-tab-security .edit-permission'); - expandSelectKit('.available-groups'); - selectKitSelectRow('staff', { selector: '.available-groups' }); - expandSelectKit('.permission-selector'); - selectKitSelectRow('2', { selector: '.permission-selector' }); + availableGroups.expand().selectRowByValue('staff'); + permissionSelector.expand().selectRowByValue('2'); click('.edit-category-tab-security .add-permission'); andThen(() => { @@ -65,21 +68,20 @@ QUnit.test("adding a permission", assert => { }); QUnit.test("adding a previously removed permission", assert => { + const availableGroups = selectKit(".available-groups"); + visit("/c/bug"); click('.edit-category'); click('li.edit-category-security a'); click('.edit-category-tab-security .edit-permission'); - expandSelectKit(".available-groups"); - click('.edit-category-tab-security .permission-list li:first-of-type .remove-permission'); andThen(() => { assert.equal(find(".edit-category-tab-security .permission-list li").length, 0, 'it removes the permission from the list'); }); - expandSelectKit('.available-groups'); - selectKitSelectRow('everyone', { selector: '.available-groups' }); + availableGroups.expand().selectRowByValue('everyone'); click('.edit-category-tab-security .add-permission'); andThen(() => { diff --git a/test/javascripts/acceptance/category-edit-test.js.es6 b/test/javascripts/acceptance/category-edit-test.js.es6 index 5f0415a43c..4f9d2da99f 100644 --- a/test/javascripts/acceptance/category-edit-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-test.js.es6 @@ -59,6 +59,8 @@ QUnit.test("Error Saving", assert => { }); QUnit.test("Subcategory list settings", assert => { + const categoryChooser = selectKit('.edit-category-tab-general .category-chooser'); + visit("/c/bug"); click('.edit-category'); @@ -69,17 +71,15 @@ QUnit.test("Subcategory list settings", assert => { }); click(".show-subcategory-list-field input[type=checkbox]"); + andThen(() => { assert.ok(visible(".subcategory-list-style-field"), "subcategory list style is shown if show subcategory list is checked"); }); click('.edit-category-general'); - - expandSelectKit('.edit-category-tab-general .category-chooser'); - - selectKitSelectRow(3, {selector: '.edit-category-tab-general .category-chooser'}); - + categoryChooser.expand().selectRowByValue(3); click('.edit-category-settings'); + andThen(() => { assert.ok(!visible(".show-subcategory-list-field"), "show subcategory list isn't visible for child categories"); assert.ok(!visible(".subcategory-list-style-field"), "subcategory list style isn't visible for child categories"); diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 index 58a95f1a87..e35505eca6 100644 --- a/test/javascripts/acceptance/composer-test.js.es6 +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -262,8 +262,8 @@ QUnit.test("Composer can toggle between edit and reply", assert => { QUnit.test("Composer can toggle between reply and createTopic", assert => { visit("/t/this-is-a-test-topic/9"); click('.topic-post:eq(0) button.reply'); - expandSelectKit('.toolbar-popup-menu-options'); - selectKitSelectRow('toggleWhisper', { selector: '.toolbar-popup-menu-options'}); + + selectKit('.toolbar-popup-menu-options').expand().selectRowByValue('toggleWhisper'); andThen(() => { assert.ok( @@ -285,8 +285,7 @@ QUnit.test("Composer can toggle between reply and createTopic", assert => { ); }); - expandSelectKit('.toolbar-popup-menu-options'); - selectKitSelectRow('toggleInvisible', { selector: '.toolbar-popup-menu-options'}); + selectKit('.toolbar-popup-menu-options').expand().selectRowByValue('toggleInvisible'); andThen(() => { assert.ok( diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index e4607f5bcf..0014cefd65 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -30,7 +30,7 @@ QUnit.test("update some fields", assert => { savePreferences(); click(".preferences-nav .nav-notifications a"); - selectDropdown('.control-group.notifications select.combobox', 1440); + selectKit('.control-group.notifications .combo-box.duration').expand().selectRowByValue(1440); savePreferences(); click(".preferences-nav .nav-categories a"); @@ -72,4 +72,4 @@ QUnit.test("email", assert => { andThen(() => { assert.equal(find('.tip.bad').text().trim(), I18n.t('user.email.invalid'), 'it should display invalid email tip'); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/search-full-test.js.es6 b/test/javascripts/acceptance/search-full-test.js.es6 index ec4a486bba..dba53a9252 100644 --- a/test/javascripts/acceptance/search-full-test.js.es6 +++ b/test/javascripts/acceptance/search-full-test.js.es6 @@ -133,13 +133,13 @@ QUnit.test("update username through advanced search ui", assert => { }); QUnit.test("update category through advanced search ui", assert => { + const categoryChooser = selectKit('.search-advanced-options .category-chooser'); + visit("/search"); + fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); - - expandSelectKit('.search-advanced-options .category-chooser'); - selectKitFillInFilter('faq', { selector: '.search-advanced-options .category-chooser' }); - selectKitSelectRow(4, { selector: '.search-advanced-options .category-chooser' }); + categoryChooser.expand().fillInFilter('faq').selectRowByValue(4); andThen(() => { assert.ok(exists('.search-advanced-options .badge-category:contains("faq")'), 'has "faq" populated'); @@ -253,45 +253,47 @@ QUnit.test("update in:seen filter through advanced search ui", assert => { }); QUnit.test("update in filter through advanced search ui", assert => { + const inSelector = selectKit('.search-advanced-options .select-kit#in'); + visit("/search"); + fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); - - expandSelectKit('.search-advanced-options .select-kit#in'); - selectKitSelectRow('bookmarks', { selector: '.search-advanced-options .select-kit#in' }); - fillIn('.search-advanced-options .select-kit#in', 'bookmarks'); + inSelector.expand().selectRowByValue('bookmarks'); andThen(() => { - assert.ok(exists(selectKit('.search-advanced-options .select-kit#in').rowByName("I bookmarked").el), 'has "I bookmarked" populated'); + assert.ok(inSelector.rowByName("I bookmarked").exists(), 'has "I bookmarked" populated'); assert.equal(find('.search input.full-page-search').val(), "none in:bookmarks", 'has updated search term to "none in:bookmarks"'); }); }); QUnit.test("update status through advanced search ui", assert => { + const statusSelector = selectKit('.search-advanced-options .select-kit#status'); + visit("/search"); + fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); - expandSelectKit('.search-advanced-options .select-kit#status'); - selectKitSelectRow('closed', { selector: '.search-advanced-options .select-kit#status' }); - fillIn('.search-advanced-options .select-kit#status', 'closed'); + statusSelector.expand().selectRowByValue('closed'); andThen(() => { - assert.ok(exists(selectKit('.search-advanced-options .select-kit#status').rowByName("are closed").el), 'has "are closed" populated'); + assert.ok(statusSelector.rowByName("are closed").exists(), 'has "are closed" populated'); assert.equal(find('.search input.full-page-search').val(), "none status:closed", 'has updated search term to "none status:closed"'); }); }); QUnit.test("update post time through advanced search ui", assert => { + const postTimeSelector = selectKit('.search-advanced-options .select-kit#postTime'); + visit("/search"); + fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); fillIn('#search-post-date .date-picker', '2016-10-05'); - expandSelectKit('.search-advanced-options .select-kit#postTime'); - selectKitSelectRow('after', { selector: '.search-advanced-options .select-kit#postTime' }); - fillIn('.search-advanced-options .select-kit#postTime', 'after'); + postTimeSelector.expand().selectRowByValue('after'); andThen(() => { - assert.ok(exists(selectKit('.search-advanced-options .select-kit#postTime').rowByName("after").el), 'has "after" populated'); + assert.ok(postTimeSelector.rowByName("after").exists(), 'has "after" populated'); assert.equal(find('.search input.full-page-search').val(), "none after:2016-10-05", 'has updated search term to "none after:2016-10-05"'); }); }); diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 index d9fb690638..5929078b28 100644 --- a/test/javascripts/acceptance/search-test.js.es6 +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -87,22 +87,24 @@ QUnit.test("Search with context", assert => { }); QUnit.test("Right filters are shown to anonymous users", assert => { + const inSelector = selectKit('.select-kit#in'); + visit("/search?expanded=true"); - expandSelectKit(".select-kit#in"); + inSelector.expand(); andThen(() => { - assert.ok(exists('.select-kit#in .select-kit-row[data-value=first]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=pinned]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=unpinned]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=wiki]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=images]')); + assert.ok(inSelector.rowByValue('first').exists()); + assert.ok(inSelector.rowByValue('pinned').exists()); + assert.ok(inSelector.rowByValue('unpinned').exists()); + assert.ok(inSelector.rowByValue('wiki').exists()); + assert.ok(inSelector.rowByValue('images').exists()); - assert.notOk(exists('.select-kit#in .select-kit-row[data-value=unseen]')); - assert.notOk(exists('.select-kit#in .select-kit-row[data-value=posted]')); - assert.notOk(exists('.select-kit#in .select-kit-row[data-value=watching]')); - assert.notOk(exists('.select-kit#in .select-kit-row[data-value=tracking]')); - assert.notOk(exists('.select-kit#in .select-kit-row[data-value=bookmarks]')); + assert.notOk(inSelector.rowByValue('unseen').exists()); + assert.notOk(inSelector.rowByValue('posted').exists()); + assert.notOk(inSelector.rowByValue('watching').exists()); + assert.notOk(inSelector.rowByValue('tracking').exists()); + assert.notOk(inSelector.rowByValue('bookmarks').exists()); assert.notOk(exists('.search-advanced-options .in-likes')); assert.notOk(exists('.search-advanced-options .in-private')); @@ -111,24 +113,26 @@ QUnit.test("Right filters are shown to anonymous users", assert => { }); QUnit.test("Right filters are shown to logged-in users", assert => { + const inSelector = selectKit('.select-kit#in'); + logIn(); Discourse.reset(); visit("/search?expanded=true"); - expandSelectKit(".select-kit#in"); + inSelector.expand(); andThen(() => { - assert.ok(exists('.select-kit#in .select-kit-row[data-value=first]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=pinned]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=unpinned]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=wiki]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=images]')); + assert.ok(inSelector.rowByValue('first').exists()); + assert.ok(inSelector.rowByValue('pinned').exists()); + assert.ok(inSelector.rowByValue('unpinned').exists()); + assert.ok(inSelector.rowByValue('wiki').exists()); + assert.ok(inSelector.rowByValue('images').exists()); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=unseen]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=posted]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=watching]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=tracking]')); - assert.ok(exists('.select-kit#in .select-kit-row[data-value=bookmarks]')); + assert.ok(inSelector.rowByValue('unseen').exists()); + assert.ok(inSelector.rowByValue('posted').exists()); + assert.ok(inSelector.rowByValue('watching').exists()); + assert.ok(inSelector.rowByValue('tracking').exists()); + assert.ok(inSelector.rowByValue('bookmarks').exists()); assert.ok(exists('.search-advanced-options .in-likes')); assert.ok(exists('.search-advanced-options .in-private')); diff --git a/test/javascripts/acceptance/topic-edit-timer-test.js.es6 b/test/javascripts/acceptance/topic-edit-timer-test.js.es6 index 588908f660..569639d810 100644 --- a/test/javascripts/acceptance/topic-edit-timer-test.js.es6 +++ b/test/javascripts/acceptance/topic-edit-timer-test.js.es6 @@ -2,32 +2,42 @@ import { acceptance } from 'helpers/qunit-helpers'; acceptance('Topic - Edit timer', { loggedIn: true }); QUnit.test('default', assert => { + const timerType = selectKit('.select-kit.timer-type'); + const futureDateInputSelector = selectKit('.future-date-input-selector'); + visit('/t/internationalization-localization'); click('.toggle-admin-menu'); click('.topic-admin-status-update button'); andThen(() => { - assert.equal(selectKit('.select-kit.timer-type').header.name(), 'Auto-Close Topic'); - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().value(), null); }); click('#private-topic-timer'); andThen(() => { - assert.equal(selectKit('.select-kit.timer-type').header.name(), 'Remind Me'); - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Select a timeframe'); + assert.equal(timerType.header().title(), 'Remind Me'); + assert.equal(timerType.header().value(), 'reminder'); + + assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().value(), null); }); }); QUnit.test('autoclose - specific time', assert => { + const futureDateInputSelector = selectKit('.future-date-input-selector'); + visit('/t/internationalization-localization'); click('.toggle-admin-menu'); click('.topic-admin-status-update button'); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('next_week', { selector: '.future-date-input-selector' }); + + futureDateInputSelector.expand().selectRowByValue('next_week'); andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Next week'); + assert.equal(futureDateInputSelector.header().title(), 'Next week'); + assert.equal(futureDateInputSelector.header().value(), 'next_week'); + const regex = /will automatically close in/g; const html = find('.future-date-input .topic-status-info').html().trim(); assert.ok(regex.test(html)); @@ -35,35 +45,44 @@ QUnit.test('autoclose - specific time', assert => { }); QUnit.test('autoclose', assert => { + const futureDateInputSelector = selectKit('.future-date-input-selector'); + visit('/t/internationalization-localization'); click('.toggle-admin-menu'); click('.topic-admin-status-update button'); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('next_week', { selector: '.future-date-input-selector' }); + futureDateInputSelector.expand().selectRowByValue('next_week'); + andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Next week'); + assert.equal(futureDateInputSelector.header().title(), 'Next week'); + assert.equal(futureDateInputSelector.header().value(), 'next_week'); + const regex = /will automatically close in/g; const html = find('.future-date-input .topic-status-info').html().trim(); assert.ok(regex.test(html)); }); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('pick_date_and_time', { selector: '.future-date-input-selector' }); + futureDateInputSelector.expand().selectRowByValue('pick_date_and_time'); + fillIn('.future-date-input .date-picker', '2099-11-24'); andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Pick date and time'); + assert.equal(futureDateInputSelector.header().title(), 'Pick date and time'); + assert.equal(futureDateInputSelector.header().value(), 'pick_date_and_time'); + const regex = /will automatically close in/g; const html = find('.future-date-input .topic-status-info').html().trim(); assert.ok(regex.test(html)); }); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('set_based_on_last_post', { selector: '.future-date-input-selector' }); + futureDateInputSelector.expand().selectRowByValue('set_based_on_last_post'); + fillIn('.future-date-input input[type=number]', '2'); + andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Close based on last post'); + assert.equal(futureDateInputSelector.header().title(), 'Close based on last post'); + assert.equal(futureDateInputSelector.header().value(), 'set_based_on_last_post'); + const regex = /This topic will close.*after the last reply/g; const html = find('.future-date-input .topic-status-info').html().trim(); assert.ok(regex.test(html)); @@ -71,32 +90,39 @@ QUnit.test('autoclose', assert => { }); QUnit.test('close temporarily', assert => { + const timerType = selectKit('.select-kit.timer-type'); + const futureDateInputSelector = selectKit('.future-date-input-selector'); + visit('/t/internationalization-localization'); click('.toggle-admin-menu'); click('.topic-admin-status-update button'); - expandSelectKit('.select-kit.timer-type'); - selectKitSelectRow('open', { selector: '.select-kit.timer-type' }); + timerType.expand().selectRowByValue('open'); andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().value(), null); }); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('next_week', { selector: '.future-date-input-selector' }); + futureDateInputSelector.expand().selectRowByValue('next_week'); + andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Next week'); + assert.equal(futureDateInputSelector.header().title(), 'Next week'); + assert.equal(futureDateInputSelector.header().value(), 'next_week'); + const regex = /will automatically open in/g; const html = find('.future-date-input .topic-status-info').html().trim(); assert.ok(regex.test(html)); }); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('pick_date_and_time', { selector: '.future-date-input-selector' }); + futureDateInputSelector.expand().selectRowByValue('pick_date_and_time'); + fillIn('.future-date-input .date-picker', '2099-11-24'); andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Pick date and time'); + assert.equal(futureDateInputSelector.header().title(), 'Pick date and time'); + assert.equal(futureDateInputSelector.header().value(), 'pick_date_and_time'); + const regex = /will automatically open in/g; const html = find('.future-date-input .topic-status-info').html().trim(); assert.ok(regex.test(html)); @@ -104,26 +130,32 @@ QUnit.test('close temporarily', assert => { }); QUnit.test('schedule', assert => { + const timerType = selectKit('.select-kit.timer-type'); + const categoryChooser = selectKit('.modal-body .category-chooser'); + const futureDateInputSelector = selectKit('.future-date-input-selector'); + visit('/t/internationalization-localization'); click('.toggle-admin-menu'); click('.topic-admin-status-update button'); - expandSelectKit('.select-kit.timer-type'); - selectKitSelectRow('publish_to_category', { selector: '.select-kit.timer-type' }); + timerType.expand().selectRowByValue('publish_to_category'); andThen(() => { - assert.equal(selectKit('.modal-body .category-chooser').header.name(), 'uncategorized'); - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Select a timeframe'); + assert.equal(categoryChooser.header().title(), 'uncategorized'); + assert.equal(categoryChooser.header().value(), null); + + assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().value(), null); }); - expandSelectKit('.modal-body .category-chooser'); - selectKitSelectRow('7', { selector: '.modal-body .category-chooser' }); + categoryChooser.expand().selectRowByValue('7'); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('next_week', { selector: '.future-date-input-selector' }); + futureDateInputSelector.expand().selectRowByValue('next_week'); andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Next week'); + assert.equal(futureDateInputSelector.header().title(), 'Next week'); + assert.equal(futureDateInputSelector.header().value(), 'next_week'); + const regex = /will be published to #dev/g; const text = find('.future-date-input .topic-status-info').text().trim(); assert.ok(regex.test(text)); @@ -131,21 +163,26 @@ QUnit.test('schedule', assert => { }); QUnit.test('auto delete', assert => { + const timerType = selectKit('.select-kit.timer-type'); + const futureDateInputSelector = selectKit('.future-date-input-selector'); + visit('/t/internationalization-localization'); click('.toggle-admin-menu'); click('.topic-admin-status-update button'); - expandSelectKit('.select-kit.timer-type'); - selectKitSelectRow('delete', { selector: '.select-kit.timer-type' }); + timerType.expand().selectRowByValue('delete'); andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal(futureDateInputSelector.header().value(), null); }); - expandSelectKit('.future-date-input-selector'); - selectKitSelectRow('two_weeks', { selector: '.future-date-input-selector' }); + futureDateInputSelector.expand().selectRowByValue('two_weeks'); + andThen(() => { - assert.equal(selectKit('.future-date-input-selector').header.name(), 'Two Weeks'); + assert.equal(futureDateInputSelector.header().title(), 'Two Weeks'); + assert.equal(futureDateInputSelector.header().value(), 'two_weeks'); + const regex = /will be automatically deleted/g; const html = find('.future-date-input .topic-status-info').html().trim(); assert.ok(regex.test(html)); diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 index 9d5935235a..4fe8877595 100644 --- a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -17,32 +17,24 @@ acceptance("Topic Notifications button", { }); QUnit.test("Updating topic notification level", assert => { - visit("/t/internationalization-localization/280"); + const notificationOptions = selectKit("#topic-footer-buttons .topic-notifications-options"); - const notificationOptions = "#topic-footer-buttons .topic-notifications-options"; + visit("/t/internationalization-localization/280"); andThen(() => { assert.ok( - exists(`${notificationOptions}`), + notificationOptions.exists(), "it should display the notification options button in the topic's footer" ); }); - expandSelectKit(notificationOptions); - selectKitSelectRow("3", { selector: notificationOptions}); + notificationOptions.expand().selectRowByValue("3"); andThen(() => { assert.equal( - selectKit(notificationOptions).selectedRow.name(), - "watching", + notificationOptions.selectedRow().name(), + "Watching", "it should display the right notification level" ); - - // This test is failing in headless mode - // assert.equal( - // find(`.timeline-footer-controls .select-kit-header`).data().name, - // 'Watching', - // 'it should display the right notification level in topic timeline' - // ); }); }); diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6 index 499fe32022..b84afe844f 100644 --- a/test/javascripts/acceptance/topic-test.js.es6 +++ b/test/javascripts/acceptance/topic-test.js.es6 @@ -48,16 +48,13 @@ QUnit.test("Showing and hiding the edit controls", assert => { }); QUnit.test("Updating the topic title and category", assert => { + const categoryChooser = selectKit('.title-wrapper .category-chooser'); + visit("/t/internationalization-localization/280"); click('#topic-title .d-icon-pencil'); - fillIn('#edit-title', 'this is the new title'); - - expandSelectKit('.title-wrapper .category-chooser'); - - selectKitSelectRow(4, {selector: '.title-wrapper .category-chooser'}); - + categoryChooser.expand().selectRowByValue(4); click('#topic-title .submit-edit'); andThen(() => { @@ -104,7 +101,7 @@ QUnit.test("Reply as new topic", assert => { "it fills composer with the ring string" ); assert.equal( - selectKit('.category-chooser').header.name(), "feature", + selectKit('.category-chooser').header().value(), "2", "it fills category selector with the right category" ); }); diff --git a/test/javascripts/components/categories-admin-dropdown-test.js.es6 b/test/javascripts/components/categories-admin-dropdown-test.js.es6 index d6c63197b6..124a4fa856 100644 --- a/test/javascripts/components/categories-admin-dropdown-test.js.es6 +++ b/test/javascripts/components/categories-admin-dropdown-test.js.es6 @@ -5,15 +5,15 @@ componentTest('default', { template: '{{categories-admin-dropdown}}', test(assert) { - const $selectKit = selectKit('.categories-admin-dropdown'); + const subject = selectKit(); - assert.equal($selectKit.el.find(".d-icon-bars").length, 1); - assert.equal($selectKit.el.find(".d-icon-caret-down").length, 1); + assert.equal(subject.el().find(".d-icon-bars").length, 1); + assert.equal(subject.el().find(".d-icon-caret-down").length, 1); - expandSelectKit(); + subject.expand(); andThen(() => { - assert.equal($selectKit.rowByValue("create").name(), "New Category"); + 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 ec29639148..bb129c872b 100644 --- a/test/javascripts/components/category-chooser-test.js.es6 +++ b/test/javascripts/components/category-chooser-test.js.es6 @@ -1,15 +1,19 @@ import componentTest from 'helpers/component-test'; -moduleForComponent('category-chooser', {integration: true}); +moduleForComponent('category-chooser', { + integration: true, + beforeEach: function() { + this.set('subject', selectKit()); + } +}); componentTest('with value', { template: '{{category-chooser value=2}}', test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), "feature"); + assert.equal(this.get('subject').header().value(), 2); + assert.equal(this.get('subject').header().title(), 'feature'); }); } }); @@ -18,11 +22,9 @@ componentTest('with excludeCategoryId', { template: '{{category-chooser excludeCategoryId=2}}', test(assert) { - expandSelectKit(); + this.get('subject').expand(); - andThen(() => { - assert.equal(selectKit('.category-chooser').rowByValue(2).el.length, 0); - }); + andThen(() => assert.notOk(this.get('subject').rowByValue(2).exists())); } }); @@ -30,12 +32,14 @@ componentTest('with scopedCategoryId', { template: '{{category-chooser scopedCategoryId=2}}', test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.category-chooser').rowByIndex(0).name(), "feature"); - assert.equal(selectKit('.category-chooser').rowByIndex(1).name(), "spec"); - assert.equal(selectKit('.category-chooser').el.find(".select-kit-row").length, 2); + assert.equal(this.get('subject').rowByIndex(0).title(), 'feature'); + assert.equal(this.get('subject').rowByIndex(0).value(), 2); + assert.equal(this.get('subject').rowByIndex(1).title(), 'spec'); + assert.equal(this.get('subject').rowByIndex(1).value(), 26); + assert.equal(this.get('subject').rows().length, 2); }); } }); @@ -48,10 +52,9 @@ componentTest('with allowUncategorized=null', { }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), "Select a category…"); + assert.equal(this.get('subject').header().value(), null); + assert.equal(this.get('subject').header().title(), "Select a category…"); }); } }); @@ -64,10 +67,9 @@ componentTest('with allowUncategorized=null rootNone=true', { }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), "Select a category…"); + assert.equal(this.get('subject').header().value(), null); + assert.equal(this.get('subject').header().title(), 'Select a category…'); }); } }); @@ -76,15 +78,14 @@ componentTest('with disallowed uncategorized, rootNone and rootNoneLabel', { template: '{{category-chooser allowUncategorized=null rootNone=true rootNoneLabel="test.root"}}', beforeEach() { - I18n.translations[I18n.locale].js.test = {root: 'root none label'}; + I18n.translations[I18n.locale].js.test = { root: 'root none label' }; this.siteSettings.allow_uncategorized_topics = false; }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), "Select a category…"); + assert.equal(this.get('subject').header().value(), null); + assert.equal(this.get('subject').header().title(), 'Select a category…'); }); } }); @@ -97,10 +98,9 @@ componentTest('with allowed uncategorized', { }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), "uncategorized"); + assert.equal(this.get('subject').header().value(), null); + assert.equal(this.get('subject').header().title(), 'uncategorized'); }); } }); @@ -113,10 +113,9 @@ componentTest('with allowed uncategorized and rootNone', { }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), "(no category)"); + assert.equal(this.get('subject').header().value(), null); + assert.equal(this.get('subject').header().title(), '(no category)'); }); } }); @@ -125,15 +124,14 @@ componentTest('with allowed uncategorized rootNone and rootNoneLabel', { template: '{{category-chooser allowUncategorized=true rootNone=true rootNoneLabel="test.root"}}', beforeEach() { - I18n.translations[I18n.locale].js.test = {root: 'root none label'}; + I18n.translations[I18n.locale].js.test = { root: 'root none label' }; this.siteSettings.allow_uncategorized_topics = true; }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit('.category-chooser').header.name(), "root none label"); + assert.equal(this.get('subject').header().value(), null); + assert.equal(this.get('subject').header().title(), 'root none label'); }); } }); diff --git a/test/javascripts/components/category-selector-test.js.es6 b/test/javascripts/components/category-selector-test.js.es6 new file mode 100644 index 0000000000..ff35b7817b --- /dev/null +++ b/test/javascripts/components/category-selector-test.js.es6 @@ -0,0 +1,87 @@ +import componentTest from 'helpers/component-test'; +import Category from "discourse/models/category"; + +moduleForComponent('category-selector', { + integration: true, + beforeEach: function() { + this.set('subject', selectKit()); + } +}); + +componentTest('default', { + template: '{{category-selector categories=categories}}', + + beforeEach() { + this.set('categories', [ Category.findById(2) ]); + }, + + 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" + ); + }); + } +}); + +componentTest('with blacklist', { + template: '{{category-selector categories=categories blacklist=blacklist}}', + + beforeEach() { + this.set('categories', [ Category.findById(2) ]); + this.set('blacklist', [ Category.findById(8) ]); + }, + + test(assert) { + 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" + ); + }); + } +}); + +componentTest('interactions', { + template: '{{category-selector categories=categories}}', + + beforeEach() { + this.set('categories', [ + Category.findById(2), + Category.findById(6) + ]); + }, + + test(assert) { + this.get('subject').expand().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); + }); + + this.get('subject').keyboard().backspace(); + this.get('subject').keyboard().backspace(); + + andThen(() => { + 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/categpry-selector-test.js.es6 b/test/javascripts/components/categpry-selector-test.js.es6 deleted file mode 100644 index 8c0925587e..0000000000 --- a/test/javascripts/components/categpry-selector-test.js.es6 +++ /dev/null @@ -1,67 +0,0 @@ -import componentTest from 'helpers/component-test'; -import Category from "discourse/models/category"; - -moduleForComponent('category-selector', {integration: true}); - -componentTest('default', { - template: '{{category-selector categories=categories}}', - - beforeEach() { - this.set('categories', [ Category.findById(2) ]); - }, - - test(assert) { - andThen(() => { - assert.propEqual(selectKit().header.name(), 'feature'); - assert.ok(!exists(".select-kit .select-kit-row[data-value='2']"), "selected categories are not in the list"); - }); - } -}); - -componentTest('with blacklist', { - template: '{{category-selector categories=categories blacklist=blacklist}}', - - beforeEach() { - this.set('categories', [ Category.findById(2) ]); - this.set('blacklist', [ Category.findById(8) ]); - }, - - test(assert) { - expandSelectKit(); - - andThen(() => { - assert.ok(exists(".select-kit .select-kit-row[data-value='6']"), "not blacklisted categories are in the list"); - assert.ok(!exists(".select-kit .select-kit-row[data-value='8']"), "blacklisted categories are not in the list"); - }); - } -}); - -componentTest('interactions', { - template: '{{category-selector categories=categories}}', - - beforeEach() { - this.set('categories', [ - Category.findById(2), - Category.findById(6) - ]); - }, - - test(assert) { - expandSelectKit(); - - selectKitSelectRow(8); - - andThen(() => { - assert.propEqual(selectKit().header.name(), 'feature,support,hosting', 'it adds the selected category'); - assert.equal(this.get('categories').length, 3); - }); - - selectKit().keyboard.backspace(); - selectKit().keyboard.backspace(); - - andThen(() => { - assert.propEqual(selectKit().header.name(), 'feature,support', '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 bf1534cf7c..e1237ccc3f 100644 --- a/test/javascripts/components/combo-box-test.js.es6 +++ b/test/javascripts/components/combo-box-test.js.es6 @@ -1,5 +1,10 @@ import componentTest from 'helpers/component-test'; -moduleForComponent('combo-box', {integration: true}); +moduleForComponent('combo-box', { + integration: true, + beforeEach: function() { + this.set('subject', selectKit()); + } +}); componentTest('default', { template: '{{combo-box content=items}}', @@ -8,12 +13,12 @@ componentTest('default', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').header.name(), "hello"); - assert.equal(selectKit('.combobox').rowByValue(1).name(), "hello"); - assert.equal(selectKit('.combobox').rowByValue(2).name(), "world"); + assert.equal(this.get('subject').header().name(), "hello"); + assert.equal(this.get('subject').rowByValue(1).name(), "hello"); + assert.equal(this.get('subject').rowByValue(2).name(), "world"); }); } }); @@ -25,11 +30,11 @@ componentTest('with valueAttribute', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').rowByValue(0).name(), "hello"); - assert.equal(selectKit('.combobox').rowByValue(1).name(), "world"); + assert.equal(this.get('subject').rowByValue(0).name(), "hello"); + assert.equal(this.get('subject').rowByValue(1).name(), "world"); }); } }); @@ -41,11 +46,11 @@ componentTest('with nameProperty', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').rowByValue(0).name(), "hello"); - assert.equal(selectKit('.combobox').rowByValue(1).name(), "world"); + assert.equal(this.get('subject').rowByValue(0).name(), "hello"); + assert.equal(this.get('subject').rowByValue(1).name(), "world"); }); } }); @@ -57,11 +62,11 @@ componentTest('with an array as content', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').rowByValue('evil').name(), "evil"); - assert.equal(selectKit('.combobox').rowByValue('trout').name(), "trout"); + assert.equal(this.get('subject').rowByValue('evil').name(), "evil"); + assert.equal(this.get('subject').rowByValue('trout').name(), "trout"); }); } }); @@ -75,17 +80,17 @@ componentTest('with value and none as a string', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').noneRow.name(), 'none'); - assert.equal(selectKit('.combobox').rowByValue("evil").name(), "evil"); - assert.equal(selectKit('.combobox').rowByValue("trout").name(), "trout"); - assert.equal(selectKit('.combobox').header.name(), 'trout'); + assert.equal(this.get('subject').noneRow().name(), 'none'); + assert.equal(this.get('subject').rowByValue("evil").name(), "evil"); + assert.equal(this.get('subject').rowByValue("trout").name(), "trout"); + assert.equal(this.get('subject').header().name(), 'trout'); assert.equal(this.get('value'), 'trout'); }); - selectKitSelectRow('__none__', {selector: '.combobox' }); + this.get('subject').selectNoneRow(); andThen(() => { assert.equal(this.get('value'), null); @@ -102,17 +107,17 @@ componentTest('with value and none as an object', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').noneRow.name(), 'none'); - assert.equal(selectKit('.combobox').rowByValue("evil").name(), "evil"); - assert.equal(selectKit('.combobox').rowByValue("trout").name(), "trout"); - assert.equal(selectKit('.combobox').header.name(), 'evil'); + assert.equal(this.get('subject').noneRow().name(), 'none'); + assert.equal(this.get('subject').rowByValue("evil").name(), "evil"); + assert.equal(this.get('subject').rowByValue("trout").name(), "trout"); + assert.equal(this.get('subject').header().name(), 'evil'); assert.equal(this.get('value'), 'evil'); }); - selectKitSelectNoneRow({ selector: '.combobox' }); + this.get('subject').selectNoneRow(); andThen(() => { assert.equal(this.get('value'), null); @@ -130,10 +135,10 @@ componentTest('with no value and none as an object', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').header.name(), 'none'); + assert.equal(this.get('subject').header().name(), 'none'); }); } }); @@ -148,10 +153,10 @@ componentTest('with no value and none string', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').header.name(), 'none'); + assert.equal(this.get('subject').header().name(), 'none'); }); } }); @@ -164,66 +169,14 @@ componentTest('with no value and no none', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').header.name(), 'evil', 'it sets the first row as value'); + assert.equal(this.get('subject').header().name(), 'evil', 'it sets the first row as value'); }); } }); -// componentTest('can be filtered', { -// template: '{{combo-box filterable=true value=1 content=content}}', -// -// beforeEach() { -// this.set("content", [{ id: 1, name: "robin"}, { id: 2, name: "regis" }]); -// }, -// -// test(assert) { -// (); -// -// andThen(() => assert.equal(find(".filter-input").length, 1, "it has a search input")); -// -// selectKitFillInFilter("regis"); -// -// andThen(() => assert.equal(selectKit().rows.length, 1, "it filters results")); -// -// selectKitFillInFilter(""); -// -// andThen(() => { -// assert.equal( -// selectKit().rows.length, 2, -// "it returns to original content when filter is empty" -// ); -// }); -// } -// }); - -// componentTest('persists filter state when expanding/collapsing', { -// template: '{{combo-box value=1 content=content filterable=true}}', -// -// beforeEach() { -// this.set("content", [{ id: 1, name: "robin" }, { id: 2, name: "régis" }]); -// }, -// -// test(assert) { -// (); -// -// selectKitFillInFilter("rob"); -// -// andThen(() => assert.equal(selectKit().rows.length, 1) ); -// -// collapseSelectKit(); -// -// andThen(() => assert.notOk(selectKit().isExpanded) ); -// -// (); -// -// andThen(() => assert.equal(selectKit().rows.length, 1) ); -// } -// }); - - componentTest('with empty string as value', { template: '{{combo-box content=items value=value}}', beforeEach() { @@ -232,10 +185,10 @@ componentTest('with empty string as value', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit('.combobox').header.name(), 'evil', 'it sets the first row as value'); + assert.equal(this.get('subject').header().name(), 'evil', 'it sets the first row as value'); }); } }); diff --git a/test/javascripts/components/list-setting-test.js.es6 b/test/javascripts/components/list-setting-test.js.es6 index 184d86a38d..b5592974a0 100644 --- a/test/javascripts/components/list-setting-test.js.es6 +++ b/test/javascripts/components/list-setting-test.js.es6 @@ -11,15 +11,14 @@ componentTest('default', { }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.propEqual(selectKit().header.name(), 'bold,italic'); + assert.equal(selectKit().header().title(), 'bold,italic'); + assert.equal(selectKit().header().value(), 'bold,italic'); }); } }); -componentTest('with emptry string as value', { +componentTest('with empty string as value', { template: '{{list-setting settingValue=settingValue}}', beforeEach() { @@ -27,10 +26,8 @@ componentTest('with emptry string as value', { }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.equal(selectKit().header.el.find(".selected-name").length, 0); + assert.equal(selectKit().header().value(), ""); }); } }); @@ -43,10 +40,8 @@ componentTest('with only setting value', { }, test(assert) { - expandSelectKit(); - andThen(() => { - assert.propEqual(selectKit().header.name(), 'bold,italic'); + assert.equal(selectKit().header().value(), 'bold,italic'); }); } }); @@ -60,31 +55,31 @@ componentTest('interactions', { }, test(assert) { - expandSelectKit(); + const listSetting = selectKit(); - selectKitSelectRow('underline'); + listSetting.expand().selectRowByValue('underline'); andThen(() => { - assert.propEqual(selectKit().header.name(), 'bold,italic,underline'); + assert.equal(listSetting.header().value(), 'bold,italic,underline'); }); - selectKitFillInFilter('strike'); + listSetting.fillInFilter('strike'); andThen(() => { - assert.equal(selectKit().highlightedRow.name(), 'strike'); + assert.equal(listSetting.highlightedRow().value(), 'strike'); }); - selectKit().keyboard.enter(); + listSetting.keyboard().enter(); andThen(() => { - assert.propEqual(selectKit().header.name(), 'bold,italic,underline,strike'); + assert.equal(listSetting.header().value(), 'bold,italic,underline,strike'); }); - selectKit().keyboard.backspace(); - selectKit().keyboard.backspace(); + listSetting.keyboard().backspace(); + listSetting.keyboard().backspace(); andThen(() => { - assert.equal(this.get('choices').length, 3, 'it removes the created content from original list'); + 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 f1c95b17ec..1bb5f940be 100644 --- a/test/javascripts/components/multi-select-test.js.es6 +++ b/test/javascripts/components/multi-select-test.js.es6 @@ -1,5 +1,10 @@ import componentTest from 'helpers/component-test'; -moduleForComponent('multi-select', {integration: true}); +moduleForComponent('multi-select', { + integration: true, + beforeEach: function() { + this.set('subject', selectKit()); + } +}); componentTest('with objects and values', { template: '{{multi-select content=items values=values}}', @@ -11,11 +16,24 @@ componentTest('with objects and values', { test(assert) { andThen(() => { - assert.propEqual(selectKit().header.name(), 'hello,world'); + assert.equal(this.get('subject').header().value(), '1,2'); }); } }); +componentTest('with title', { + template: '{{multi-select title=(i18n "test.title")}}', + + beforeEach() { + I18n.translations[I18n.locale].js.test = {title: 'My title'}; + }, + + test(assert) { + andThen(() => assert.equal(selectKit().header().title(), 'My title') ); + } +}); + + componentTest('interactions', { template: '{{multi-select none=none content=items values=values}}', @@ -26,84 +44,104 @@ componentTest('interactions', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().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'); andThen(() => { - assert.equal(selectKit().noneRow.el.length, 1); - assert.equal(selectKit().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' + ); }); - selectKitSelectRow(3); + this.get('subject').selectRowByValue(3); andThen(() => { - assert.equal(selectKit().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' + ); }); - selectKitFillInFilter('joffrey'); + this.get('subject').fillInFilter('joffrey'); andThen(() => { - assert.equal(selectKit().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' + ); }); - selectKit().keyboard.enter(); + this.get('subject').keyboard().enter(); andThen(() => { - assert.equal(selectKit().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' + ); }); - selectKit().keyboard.backspace(); + this.get('subject').keyboard().backspace(); andThen(() => { - const $lastSelectedName = selectKit().header.el.find('.selected-name').last(); + 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'); }); - selectKit().keyboard.backspace(); + this.get('subject').keyboard().backspace(); andThen(() => { - const $lastSelectedName = selectKit().header.el.find('.selected-name').last(); + 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(exists(selectKit().rowByValue('joffrey').el), 'generated content shouldn’t appear in content when removed'); + assert.notOk(this.get('subject').rowByValue('joffrey').exists(), 'generated content shouldn’t appear in content when removed'); }); - selectKit().keyboard.selectAll(); + this.get('subject').keyboard().selectAll(); andThen(() => { - const $highlightedSelectedNames = selectKit().header.el.find('.selected-name.is-highlighted'); + const $highlightedSelectedNames = this.get('subject').header().el().find('.selected-name.is-highlighted'); assert.equal($highlightedSelectedNames.length, 3, 'it highlights each selected name'); }); - selectKit().keyboard.backspace(); + this.get('subject').keyboard().backspace(); andThen(() => { - const $selectedNames = selectKit().header.el.find('.selected-name'); + const $selectedNames = this.get('subject').header().el().find('.selected-name'); assert.equal($selectedNames.length, 0, 'it removed all selected content'); }); andThen(() => { - assert.ok(this.$(".select-kit").hasClass("is-focused")); - assert.ok(this.$(".select-kit").hasClass("is-expanded")); + assert.ok(this.get('subject').isFocused()); + assert.ok(this.get('subject').isExpanded()); }); - selectKit().keyboard.escape(); + this.get('subject').keyboard().escape(); andThen(() => { - assert.ok(this.$(".select-kit").hasClass("is-focused")); - assert.notOk(this.$(".select-kit").hasClass("is-expanded")); + assert.ok(this.get('subject').isFocused()); + assert.notOk(this.get('subject').isExpanded()); }); - selectKit().keyboard.escape(); + this.get('subject').keyboard().escape(); andThen(() => { - assert.notOk(this.$(".select-kit").hasClass("is-focused")); - assert.notOk(this.$(".select-kit").hasClass("is-expanded")); + assert.notOk(this.get('subject').isFocused()); + assert.notOk(this.get('subject').isExpanded()); }); } }); diff --git a/test/javascripts/components/pinned-options-test.js.es6 b/test/javascripts/components/pinned-options-test.js.es6 index 3808b63dca..87aafc07d2 100644 --- a/test/javascripts/components/pinned-options-test.js.es6 +++ b/test/javascripts/components/pinned-options-test.js.es6 @@ -10,7 +10,12 @@ const buildTopic = function() { }); }; -moduleForComponent('pinned-options', { integration: true }); +moduleForComponent('pinned-options', { + integration: true, + beforeEach: function() { + this.set('subject', selectKit()); + } +}); componentTest('updating the content refreshes the list', { template: '{{pinned-options value=pinned topic=topic}}', @@ -22,14 +27,14 @@ componentTest('updating the content refreshes the list', { }, test(assert) { - expandSelectKit(); - - andThen(() => assert.equal(selectKit().header.name(), "Pinned") ); + andThen(() => { + assert.equal(this.get('subject').header().name(), "pinned"); + }); andThen(() => this.set("pinned", false)); andThen(() => { - assert.equal(selectKit().header.name(), "Unpinned"); + assert.equal(this.get('subject').header().name(), "unpinned"); }); } }); diff --git a/test/javascripts/components/single-select-test.js.es6 b/test/javascripts/components/single-select-test.js.es6 index 2e9946f672..f79b2b5182 100644 --- a/test/javascripts/components/single-select-test.js.es6 +++ b/test/javascripts/components/single-select-test.js.es6 @@ -2,7 +2,12 @@ import componentTest from 'helpers/component-test'; import { withPluginApi } from 'discourse/lib/plugin-api'; import { clearCallbacks } from 'select-kit/mixins/plugin-api'; -moduleForComponent('single-select', { integration: true }); +moduleForComponent('single-select', { + integration: true, + beforeEach: function() { + this.set('subject', selectKit()); + } +}); componentTest('updating the content refreshes the list', { template: '{{single-select value=1 content=content}}', @@ -12,10 +17,10 @@ componentTest('updating the content refreshes the list', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().rowByValue(1).name(), "BEFORE"); + assert.equal(this.get('subject').rowByValue(1).name(), "BEFORE"); }); andThen(() => { @@ -23,7 +28,7 @@ componentTest('updating the content refreshes the list', { }); andThen(() => { - assert.equal(selectKit().rowByValue(1).name(), "AFTER"); + assert.equal(this.get('subject').rowByValue(1).name(), "AFTER"); }); } }); @@ -37,16 +42,16 @@ componentTest('accepts a value by reference', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { assert.equal( - selectKit().selectedRow.name(), "robin", + this.get('subject').selectedRow().name(), "robin", "it highlights the row corresponding to the value" ); }); - selectKitSelectRow(1); + this.get('subject').selectRowByValue(1); andThen(() => { assert.equal(this.get("value"), 1, "it mutates the value"); @@ -58,7 +63,11 @@ componentTest('no default icon', { template: '{{single-select}}', test(assert) { - assert.equal(selectKit().header.icon().length, 0, "it doesn’t have an icon if not specified"); + assert.equal( + this.get('subject').header().icon().length, + 0, + "it doesn’t have an icon if not specified" + ); } }); @@ -66,10 +75,10 @@ componentTest('default search icon', { template: '{{single-select filterable=true}}', test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.ok(exists(selectKit().filter.icon), "it has a the correct icon"); + assert.ok(exists(this.get('subject').filter().icon()), "it has an icon"); }); } }); @@ -78,10 +87,10 @@ componentTest('with no search icon', { template: '{{single-select filterable=true filterIcon=null}}', test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().filter.icon().length, 0, "it has no icon"); + assert.notOk(exists(this.get('subject').filter().icon()), "it has no icon"); }); } }); @@ -90,10 +99,13 @@ componentTest('custom search icon', { template: '{{single-select filterable=true filterIcon="shower"}}', test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.ok(selectKit().filter.icon().hasClass("fa-shower"), "it has a the correct icon"); + assert.ok( + this.get('subject').filter().icon().hasClass("fa-shower"), + "it has a the correct icon" + ); }); } }); @@ -101,13 +113,13 @@ componentTest('custom search icon', { componentTest('is expandable', { template: '{{single-select}}', test(assert) { - expandSelectKit(); + this.get('subject').expand(); - andThen(() => assert.ok(selectKit().isExpanded) ); + andThen(() => assert.ok(this.get('subject').isExpanded()) ); - collapseSelectKit(); + this.get('subject').collapse(); - andThen(() => assert.notOk(selectKit().isExpanded) ); + andThen(() => assert.notOk(this.get('subject').isExpanded()) ); } }); @@ -120,10 +132,10 @@ componentTest('accepts custom value/name keys', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().selectedRow.name(), "robin"); + assert.equal(this.get('subject').selectedRow().name(), "robin"); }); } }); @@ -138,7 +150,7 @@ componentTest('doesn’t render collection content before first expand', { test(assert) { assert.notOk(exists(find(".select-kit-collection"))); - expandSelectKit(); + this.get('subject').expand(); andThen(() => { assert.ok(exists(find(".select-kit-collection"))); @@ -154,7 +166,7 @@ componentTest('supports options to limit size', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { const height = find(".select-kit-collection").height(); @@ -171,16 +183,20 @@ componentTest('dynamic headerText', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().header.name(), "robin"); + assert.equal(this.get('subject').header().name(), 'robin'); }); - selectKitSelectRow(2); + this.get('subject').selectRowByValue(2); andThen(() => { - assert.equal(selectKit().header.name(), "regis", "it changes header text"); + assert.equal( + this.get('subject').header().name(), + 'regis', + 'it changes header text' + ); }); } }); @@ -196,9 +212,13 @@ componentTest('supports custom row template', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); - andThen(() => assert.equal(selectKit().rowByValue(1).el.html().trim(), "robin") ); + andThen(() => { + assert.equal( + this.get('subject').rowByValue(1).el().html().trim(), "robin" + ); + }); } }); @@ -206,22 +226,25 @@ componentTest('supports converting select value to integer', { template: '{{single-select value=value content=content castInteger=true}}', beforeEach() { - this.set("value", 2); - this.set("content", [{ id: "1", name: "robin"}, {id: "2", name: "régis" }]); + this.set('value', 2); + this.set('content', [{ id: '1', name: 'robin'}, {id: '2', name: 'régis' }]); }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); - andThen(() => assert.equal(selectKit().selectedRow.name(), "régis") ); + andThen(() => assert.equal(this.get('subject').selectedRow().name(), 'régis') ); andThen(() => { - this.set("value", 3); - this.set("content", [{ id: "3", name: "jeff" }]); + this.set('value', 1); }); andThen(() => { - assert.equal(selectKit().selectedRow.name(), "jeff", "it works with dynamic content"); + assert.equal( + this.get('subject').selectedRow().name(), + 'robin', + 'it works with dynamic content' + ); }); } }); @@ -234,49 +257,41 @@ componentTest('supports keyboard events', { }, test(assert) { - expandSelectKit(); - - selectKit().keyboard.down(); + this.get('subject').expand().keyboard().down(); andThen(() => { - assert.equal(selectKit().highlightedRow.title(), "regis", "the next row is highlighted"); + assert.equal(this.get('subject').highlightedRow().title(), "regis", "the next row is highlighted"); }); - selectKit().keyboard.down(); + this.get('subject').keyboard().down(); andThen(() => { - assert.equal(selectKit().highlightedRow.title(), "robin", "it returns to the first row"); + assert.equal(this.get('subject').highlightedRow().title(), "robin", "it returns to the first row"); }); - selectKit().keyboard.up(); + this.get('subject').keyboard().up(); andThen(() => { - assert.equal(selectKit().highlightedRow.title(), "regis", "it highlights the last row"); + assert.equal(this.get('subject').highlightedRow().title(), "regis", "it highlights the last row"); }); - selectKit().keyboard.enter(); + this.get('subject').keyboard().enter(); andThen(() => { - assert.equal(selectKit().selectedRow.title(), "regis", "it selects the row when pressing enter"); - assert.notOk(selectKit().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"); }); - expandSelectKit(); - - selectKit().keyboard.escape(); + this.get('subject').expand().keyboard().escape(); andThen(() => { - assert.notOk(selectKit().isExpanded, "it collapses the select box"); + assert.notOk(this.get('subject').isExpanded(), "it collapses the select box"); }); - expandSelectKit(); - - selectKitFillInFilter("regis"); - - selectKit().keyboard.tab(); + this.get('subject').expand().fillInFilter('regis').keyboard().tab(); andThen(() => { - assert.notOk(selectKit().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"); }); } }); @@ -302,18 +317,18 @@ componentTest('support appending content through plugin api', { beforeEach() { withPluginApi('0.8.13', api => { - api.modifySelectKit("select-kit") - .appendContent([{ id: "2", name: "regis"}]); + api.modifySelectKit('select-kit') + .appendContent([{ id: '2', name: 'regis'}]); }); - this.set("content", [{ id: "1", name: "robin"}]); + this.set('content', [{ id: '1', name: 'robin'}]); }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().rows.length, 2); - assert.equal(selectKit().rows.eq(1).data("name"), "regis"); + assert.equal(this.get('subject').rows().length, 2); + assert.equal(this.get('subject').rowByIndex(1).name(), 'regis'); }); andThen(() => clearCallbacks()); @@ -336,11 +351,11 @@ componentTest('support modifying content through plugin api', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().rows.length, 3); - assert.equal(selectKit().rows.eq(1).data("name"), "sam"); + assert.equal(this.get('subject').rows().length, 3); + assert.equal(this.get('subject').rowByIndex(1).name(), "sam"); }); andThen(() => clearCallbacks()); @@ -360,11 +375,11 @@ componentTest('support prepending content through plugin api', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().rows.length, 2); - assert.equal(selectKit().rows.eq(0).data("name"), "regis"); + assert.equal(this.get('subject').rows().length, 2); + assert.equal(this.get('subject').rowByIndex(0).name(), "regis"); }); andThen(() => clearCallbacks()); @@ -387,9 +402,7 @@ componentTest('support modifying on select behavior through plugin api', { }, test(assert) { - expandSelectKit(); - - selectKitSelectRow(1); + this.get('subject').expand().selectRowByValue(1); andThen(() => { assert.equal(find(".on-select-test").html(), "1"); @@ -408,10 +421,10 @@ componentTest('with nameChanges', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().header.name(), "robin"); + assert.equal(this.get('subject').header().name(), "robin"); }); andThen(() => { @@ -419,7 +432,7 @@ componentTest('with nameChanges', { }); andThen(() => { - assert.equal(selectKit().header.name(), "robin2"); + assert.equal(this.get('subject').header().name(), "robin2"); }); } }); @@ -433,10 +446,11 @@ componentTest('with null value', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().header.name(), "robin"); + assert.equal(this.get('subject').header().name(), "robin"); + assert.equal(this.get('subject').header().value(), undefined); }); } }); @@ -449,7 +463,8 @@ componentTest('with collection header', { }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); + andThen(() => assert.ok(exists(".collection-header h2"))); } }); @@ -463,7 +478,9 @@ componentTest('with onToggle', { test(assert) { andThen(() => assert.notOk(exists(".onToggleTest"))); - expandSelectKit(); + + this.get('subject').expand(); + andThen(() => assert.ok(exists(".onToggleTest"))); } }); @@ -477,7 +494,9 @@ componentTest('with onExpand', { test(assert) { andThen(() => assert.notOk(exists(".onExpandTest"))); - expandSelectKit(); + + this.get('subject').expand(); + andThen(() => assert.ok(exists(".onExpandTest"))); } }); @@ -491,9 +510,25 @@ componentTest('with onCollapse', { test(assert) { andThen(() => assert.notOk(exists(".onCollapseTest"))); - expandSelectKit(); + + this.get('subject').expand(); + andThen(() => assert.notOk(exists(".onCollapseTest"))); - collapseSelectKit(); + + this.get('subject').collapse(); + andThen(() => assert.ok(exists(".onCollapseTest"))); } }); + +componentTest('with title', { + template: '{{single-select title=(i18n "test.title")}}', + + beforeEach() { + I18n.translations[I18n.locale].js.test = {title: 'My title'}; + }, + + test(assert) { + andThen(() => assert.equal(this.get('subject').header().title(), 'My title') ); + } +}); 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 9a5534c3cb..3bdd8ec4fc 100644 --- a/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 +++ b/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 @@ -8,28 +8,37 @@ const buildTopic = function() { }); }; -moduleForComponent('topic-footer-mobile-dropdown', {integration: true}); +moduleForComponent('topic-footer-mobile-dropdown', { + integration: true, + beforeEach: function() { + this.set('subject', selectKit()); + } +}); componentTest('default', { template: '{{topic-footer-mobile-dropdown topic=topic}}', beforeEach() { - this.set("topic", buildTopic()); + this.set('topic', buildTopic()); }, test(assert) { - expandSelectKit(); + this.get('subject').expand(); andThen(() => { - assert.equal(selectKit().header.name(), "Topic Controls"); - assert.equal(selectKit().rowByIndex(0).name(), "Bookmark"); - assert.equal(selectKit().rowByIndex(1).name(), "Share"); - assert.equal(selectKit().selectedRow.el.length, 0, "it doesn’t preselect first row"); + assert.equal(this.get('subject').header().title(), 'Topic Controls'); + assert.equal(this.get('subject').header().value(), null); + assert.equal(this.get('subject').rowByIndex(0).name(), 'Bookmark'); + assert.equal(this.get('subject').rowByIndex(1).name(), 'Share'); + assert.notOk( + this.get('subject').selectedRow().exists(), + 'it doesn’t preselect first row' + ); }); - selectKitSelectRow("share"); + this.get('subject').selectRowByValue('share'); andThen(() => { - assert.equal(this.get("value"), null, "it resets the value"); + assert.equal(this.get('value'), null, 'it resets the value'); }); } }); diff --git a/test/javascripts/components/topic-notifications-button-test.js.es6 b/test/javascripts/components/topic-notifications-button-test.js.es6 index 0d41471e31..8dac873939 100644 --- a/test/javascripts/components/topic-notifications-button-test.js.es6 +++ b/test/javascripts/components/topic-notifications-button-test.js.es6 @@ -22,13 +22,13 @@ componentTest('the header has a localized title', { test(assert) { andThen(() => { - assert.equal(selectKit().header.name(), "Normal", "it has the correct title"); + assert.equal(selectKit().header().name(), "Normal", "it has the correct title"); }); this.set("topic", buildTopic(2)); andThen(() => { - assert.equal(selectKit().header.name(), "Tracking", "it correctly changes the title"); + assert.equal(selectKit().header().name(), "Tracking", "it correctly changes the title"); }); } }); diff --git a/test/javascripts/helpers/assertions.js b/test/javascripts/helpers/assertions.js index 4e6b53b5d1..95d7b2a7dd 100644 --- a/test/javascripts/helpers/assertions.js +++ b/test/javascripts/helpers/assertions.js @@ -12,12 +12,6 @@ function visible(selector) { return find(selector + ":visible").length > 0; } -Ember.Test.registerAsyncHelper('selectDropdown', function(app, selector, itemId) { - var $select2 = find(selector); - $select2.select2('val', itemId.toString()); - $select2.trigger("change"); -}); - function invisible(selector) { var $items = find(selector + ":visible"); return $items.length === 0 || diff --git a/test/javascripts/helpers/select-kit-helper.js b/test/javascripts/helpers/select-kit-helper.js index 80c6373606..be2debd6a8 100644 --- a/test/javascripts/helpers/select-kit-helper.js +++ b/test/javascripts/helpers/select-kit-helper.js @@ -11,52 +11,42 @@ function checkSelectKitIsNotCollapsed(selector) { } Ember.Test.registerAsyncHelper('expandSelectKit', function(app, selector) { - selector = selector || '.select-kit'; - checkSelectKitIsNotExpanded(selector); - click(selector + ' .select-kit-header'); }); Ember.Test.registerAsyncHelper('collapseSelectKit', function(app, selector) { - selector = selector || '.select-kit'; - checkSelectKitIsNotCollapsed(selector); - click(selector + ' .select-kit-header'); }); -Ember.Test.registerAsyncHelper('selectKitSelectRow', function(app, rowValue, options) { - options = options || {}; - options.selector = options.selector || '.select-kit'; - - checkSelectKitIsNotCollapsed(options.selector); - - click(options.selector + " .select-kit-row[data-value='" + rowValue + "']"); +Ember.Test.registerAsyncHelper('selectKitFillInFilter', function(app, filter, selector) { + checkSelectKitIsNotCollapsed(selector); + fillIn(selector + ' .filter-input', filter); }); -Ember.Test.registerAsyncHelper('selectKitSelectNoneRow', function(app, options) { - options = options || {}; - options.selector = options.selector || '.select-kit'; - - checkSelectKitIsNotCollapsed(options.selector); - - click(options.selector + " .select-kit-row.none"); +Ember.Test.registerAsyncHelper('selectKitSelectRowByValue', function(app, value, selector) { + checkSelectKitIsNotCollapsed(selector); + click(selector + " .select-kit-row[data-value='" + value + "']"); }); -Ember.Test.registerAsyncHelper('selectKitFillInFilter', function(app, filter, options) { - options = options || {}; - options.selector = options.selector || '.select-kit'; +Ember.Test.registerAsyncHelper('selectKitSelectRowByName', function(app, name, selector) { + checkSelectKitIsNotCollapsed(selector); + click(selector + " .select-kit-row[data-name='" + name + "']"); +}); - checkSelectKitIsNotCollapsed(options.selector); - - var filterQuerySelector = options.selector + ' .filter-input'; - fillIn(filterQuerySelector, filter); +Ember.Test.registerAsyncHelper('selectKitSelectNoneRow', function(app, selector) { + checkSelectKitIsNotCollapsed(selector); + click(selector + " .select-kit-row.none"); +}); +Ember.Test.registerAsyncHelper('selectKitSelectRowByIndex', function(app, index, selector) { + checkSelectKitIsNotCollapsed(selector); + click(find(selector + " .select-kit-row").eq(index)); }); function selectKit(selector) { // eslint-disable-line no-unused-vars - selector = selector || '.select-kit'; + selector = selector || ".select-kit"; function rowHelper(row) { return { @@ -64,18 +54,18 @@ function selectKit(selector) { // eslint-disable-line no-unused-vars icon: function() { return row.find('.d-icon'); }, title: function() { return row.attr('title'); }, value: function() { return row.attr('data-value'); }, - el: row + exists: function() { return exists(row); }, + el: function() { return row; } }; } function headerHelper(header) { return { - name: function() { - return header.attr('data-name'); - }, + value: function() { return header.attr('data-value'); }, + name: function() { return header.attr('data-name'); }, icon: function() { return header.find('.icon'); }, title: function() { return header.attr('title'); }, - el: header + el: function() { return header; } }; } @@ -83,14 +73,14 @@ function selectKit(selector) { // eslint-disable-line no-unused-vars return { icon: function() { return filter.find('.d-icon'); }, exists: function() { return exists(filter); }, - el: filter + el: function() { return filter; } }; } - function keyboardHelper() { + function keyboardHelper(eventSelector) { function createEvent(target, keyCode, options) { target = target || ".filter-input"; - selector = find(selector).find(target); + eventSelector = find(eventSelector).find(target); options = options || {}; andThen(function() { @@ -98,7 +88,7 @@ function selectKit(selector) { // eslint-disable-line no-unused-vars var event = jQuery.Event(type); event.keyCode = keyCode; if (options && options.metaKey === true) { event.metaKey = true; } - find(selector).trigger(event); + find(eventSelector).trigger(event); }); } @@ -111,20 +101,69 @@ function selectKit(selector) { // eslint-disable-line no-unused-vars backspace: function(target) { createEvent(target, 8); }, selectAll: function(target) { createEvent(target, 65, {metaKey: true}); }, }; - } + }; return { - keyboard: keyboardHelper(), + expand: function() { + expandSelectKit(selector); + return selectKit(selector); + }, - isExpanded: find(selector).hasClass('is-expanded'), + collapse: function() { + collapseSelectKit(selector); + return selectKit(selector); + }, - isHidden: find(selector).hasClass('is-hidden'), + selectRowByIndex: function(index) { + selectKitSelectRowByIndex(index, selector); + return selectKit(selector); + }, - header: headerHelper(find(selector).find('.select-kit-header')), + selectRowByValue: function(value) { + selectKitSelectRowByValue(value, selector); + return selectKit(selector); + }, - filter: filterHelper(find(selector).find('.select-kit-filter')), + selectRowByName: function(name) { + selectKitSelectRowByValue(name, selector); + return selectKit(selector); + }, - rows: find(selector).find('.select-kit-row'), + selectNoneRow: function() { + selectKitSelectNoneRow(selector); + return selectKit(selector); + }, + + fillInFilter: function(filter) { + selectKitFillInFilter(filter, selector); + return selectKit(selector); + }, + + keyboard: function() { return keyboardHelper(selector); }, + + isExpanded: function() { + return find(selector).hasClass('is-expanded'); + }, + + isFocused: function() { + return find(selector).hasClass('is-focused'); + }, + + isHidden: function() { + return find(selector).hasClass('is-hidden'); + }, + + header: function() { + return headerHelper(find(selector).find('.select-kit-header')); + }, + + filter: function() { + return filterHelper(find(selector).find('.select-kit-filter')); + }, + + rows: function() { + return find(selector).find('.select-kit-row'); + }, rowByValue: function(value) { return rowHelper(find(selector).find('.select-kit-row[data-value="' + value + '"]')); @@ -138,12 +177,20 @@ function selectKit(selector) { // eslint-disable-line no-unused-vars return rowHelper(find(selector).find('.select-kit-row:eq(' + index + ')')); }, - el: find(selector), + el: function() { return find(selector); }, - noneRow: rowHelper(find(selector).find('.select-kit-row.none')), + noneRow: function() { + return rowHelper(find(selector).find('.select-kit-row.none')); + }, - selectedRow: rowHelper(find(selector).find('.select-kit-row.is-selected')), + selectedRow: function() { + return rowHelper(find(selector).find('.select-kit-row.is-selected')); + }, - highlightedRow: rowHelper(find(selector).find('.select-kit-row.is-highlighted')) + highlightedRow: function() { + return rowHelper(find(selector).find('.select-kit-row.is-highlighted')); + }, + + exists: function() { return exists(selector); } }; } From ac1e93e82a4cb4444782a25b0820a6adab4092d7 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 22 Dec 2017 19:05:40 +0530 Subject: [PATCH 041/106] Minor fix to correctly trim spaces in HTML to Markdown conversion --- .../discourse/lib/to-markdown.js.es6 | 23 ++++++++++++------- test/javascripts/lib/to-markdown-test.js.es6 | 4 ++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 index 22eb2b56d6..2f236392fe 100644 --- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 +++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 @@ -4,15 +4,20 @@ const trimLeft = text => text.replace(/^\s+/,""); const trimRight = text => text.replace(/\s+$/,""); class Tag { - constructor(name, prefix = "", suffix = "") { + constructor(name, prefix = "", suffix = "", inline = false) { this.name = name; this.prefix = prefix; this.suffix = suffix; + this.inline = inline; } decorate(text) { if (this.prefix || this.suffix) { - return [this.prefix, text, this.suffix].join(""); + text = [this.prefix, text, this.suffix].join(""); + } + + if (this.inline) { + text = " " + text + " "; } return text; @@ -69,7 +74,7 @@ class Tag { static emphasis(name, decorator) { return class extends Tag { constructor() { - super(name, decorator, decorator); + super(name, decorator, decorator, true); } decorate(text) { @@ -112,7 +117,7 @@ class Tag { static link() { return class extends Tag { constructor() { - super("a"); + super("a", "", "", true); } decorate(text) { @@ -131,7 +136,7 @@ class Tag { static image() { return class extends Tag { constructor() { - super("img"); + super("img", "", "", true); } toMarkdown() { @@ -193,7 +198,7 @@ class Tag { static li() { return class extends Tag.slice("li", "\n") { decorate(text) { - const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => " ").join(""); + const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => "\t").join(""); return super.decorate(`${indent}* ${trimLeft(text)}`); } }; @@ -209,6 +214,8 @@ class Tag { if (this.element.parentNames.includes("pre")) { this.prefix = '\n\n```\n'; this.suffix = '\n```\n\n'; + } else { + this.inline = true; } text = $('