diff --git a/.eslintignore b/.eslintignore index c7910a26c0..afeff2c869 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ +app/assets/javascripts/discourse-loader.js app/assets/javascripts/env.js app/assets/javascripts/main_include_admin.js app/assets/javascripts/vendor.js diff --git a/.eslintrc b/.eslintrc index 320310db8d..e05e8de173 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,9 +1,7 @@ { "extends": "eslint-config-discourse", "rules": { - "discourse-ember/global-ember": 2, - "no-duplicate-imports": 2, - "sort-imports": 2 + "discourse-ember/global-ember": 2 }, "globals": { "moduleFor": "off", @@ -14,6 +12,6 @@ "currentURL": "off", "invisible": "off", "visible": "off", - "count": "off", + "count": "off" } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e4c2b6ac1..7583ed8011 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: if: env.BUILD_TYPE != 'LINT' run: | sudo apt-get update - sudo apt-get -yqq install postgresql-client libpq-dev jpegoptim optipng jhead + sudo apt-get -yqq install postgresql-client libpq-dev jpegoptim optipng jhead pngcrush pngquant wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh - name: Update imagemagick diff --git a/.prettierignore b/.prettierignore index ca81e935a8..b8874cd71c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ config/locales/**/*.yml !config/locales/**/*.en*.yml script/import_scripts/**/*.yml +app/assets/javascripts/discourse-loader.js app/assets/javascripts/env.js app/assets/javascripts/main_include_admin.js app/assets/javascripts/vendor.js diff --git a/Gemfile b/Gemfile index 3ea59bc92e..80bf241db0 100644 --- a/Gemfile +++ b/Gemfile @@ -167,6 +167,7 @@ group :test, :development do gem 'parallel_tests' gem 'rswag-specs' + gem 'json_schemer' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 3b16e87e10..afa3ff6c3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,7 +43,7 @@ GEM annotate (3.1.1) activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 14.0) - ast (2.4.1) + ast (2.4.2) aws-eventstream (1.1.0) aws-partitions (1.390.0) aws-sdk-core (3.109.2) @@ -72,10 +72,10 @@ GEM rack (>= 0.9.0) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.5.1) + bootsnap (1.6.0) msgpack (~> 1.0) builder (3.2.4) - bullet (6.1.2) + bullet (6.1.3) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.1.3) @@ -93,7 +93,7 @@ GEM crack (0.4.5) rexml crass (1.0.6) - css_parser (1.7.1) + css_parser (1.8.0) addressable debug_inspector (1.0.0) diff-lcs (1.4.4) @@ -114,6 +114,8 @@ GEM in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) docile (1.3.5) + ecma-re-validator (0.3.0) + regexp_parser (~> 2.0) email_reply_trimmer (0.1.13) ember-data-source (3.0.2) ember-source (>= 2, < 3.0) @@ -141,6 +143,7 @@ GEM globalid (0.4.2) activesupport (>= 4.2.0) guess_html_encoding (0.0.11) + hana (1.3.7) hashdiff (1.0.1) hashie (4.1.0) highline (2.0.3) @@ -159,9 +162,15 @@ GEM json (2.5.1) json-schema (2.8.1) addressable (>= 2.4) + json_schemer (0.2.17) + ecma-re-validator (~> 0.3) + hana (~> 1.3) + regexp_parser (~> 2.0) + uri_template (~> 0.7) jwt (2.2.2) kgio (2.11.3) libv8 (8.4.255.0) + libv8 (8.4.255.0-universal-darwin-20) libv8 (8.4.255.0-x86_64-darwin-18) libv8 (8.4.255.0-x86_64-darwin-19) libv8 (8.4.255.0-x86_64-darwin-20) @@ -177,7 +186,7 @@ GEM logstash-event (1.2.02) logstash-logger (0.26.1) logstash-event (~> 1.2) - logster (2.9.4) + logster (2.9.5) loofah (2.9.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -197,13 +206,13 @@ GEM mini_scheduler (0.13.0) sidekiq (>= 4.2.3) mini_sql (1.0.1) - mini_suffix (0.3.0) + mini_suffix (0.3.2) ffi (~> 1.9) minitest (5.14.3) mocha (1.12.0) mock_redis (0.27.3) ruby2_keywords - msgpack (1.3.3) + msgpack (1.4.1) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) @@ -212,6 +221,8 @@ GEM nokogiri (1.11.1) mini_portile2 (~> 2.5.0) racc (~> 1.4) + nokogiri (1.11.1-arm64-darwin) + racc (~> 1.4) nokogiri (1.11.1-x86_64-darwin) racc (~> 1.4) nokogiri (1.11.1-x86_64-linux) @@ -225,7 +236,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oj (3.11.0) + oj (3.11.1) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -273,7 +284,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.1.1) + puma (5.2.0) nio4r (~> 2.0) r2 (0.2.7) racc (1.5.2) @@ -364,7 +375,7 @@ GEM rubocop-ast (>= 1.2.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.4.0) + rubocop-ast (1.4.1) parser (>= 2.7.1.5) rubocop-discourse (2.4.1) rubocop (>= 1.1.0) @@ -395,9 +406,9 @@ GEM seed-fu (2.3.9) activerecord (>= 3.1) activesupport (>= 3.1) - shoulda-matchers (4.5.0) + shoulda-matchers (4.5.1) activesupport (>= 4.2.0) - sidekiq (6.1.2) + sidekiq (6.1.3) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) @@ -416,7 +427,7 @@ GEM sprockets (>= 3.0.0) sshkey (2.0.0) stackprof (0.2.16) - test-prof (0.12.2) + test-prof (1.0.0) thor (1.1.0) thread_safe (0.3.6) tilt (2.0.10) @@ -432,6 +443,7 @@ GEM kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.13.2) + uri_template (0.7.0) webmock (3.11.1) addressable (>= 2.3.6) crack (>= 0.3.2) @@ -444,6 +456,7 @@ GEM zeitwerk (2.4.2) PLATFORMS + arm64-darwin-20 ruby x86_64-darwin-18 x86_64-darwin-19 @@ -494,6 +507,7 @@ DEPENDENCIES htmlentities http_accept_language json + json_schemer listen lograge logstash-event @@ -575,4 +589,4 @@ DEPENDENCIES yaml-lint BUNDLED WITH - 2.2.3 + 2.2.6 diff --git a/app/assets/javascripts/admin/addon/components/ace-editor.js b/app/assets/javascripts/admin/addon/components/ace-editor.js index 4f70345003..862d611188 100644 --- a/app/assets/javascripts/admin/addon/components/ace-editor.js +++ b/app/assets/javascripts/admin/addon/components/ace-editor.js @@ -10,6 +10,7 @@ export default Component.extend({ _editor: null, _skipContentChangeEvent: null, disabled: false, + htmlPlaceholder: false, @observes("editorId") editorIdChanged() { @@ -86,6 +87,10 @@ export default Component.extend({ loadedAce.config.set("loadWorkerFromBlob", false); loadedAce.config.set("workerPath", getURL("/javascripts/ace")); // Do not use CDN for workers + if (this.htmlPlaceholder) { + this._overridePlaceholder(loadedAce); + } + if (!this.element || this.isDestroying || this.isDestroyed) { return; } @@ -131,4 +136,32 @@ export default Component.extend({ } }, }, + + _overridePlaceholder(loadedAce) { + const originalPlaceholderSetter = + loadedAce.config.$defaultOptions.editor.placeholder.set; + + loadedAce.config.$defaultOptions.editor.placeholder.set = function () { + if (!this.$updatePlaceholder) { + const originalRendererOn = this.renderer.on; + this.renderer.on = function () {}; + originalPlaceholderSetter.call(this, ...arguments); + this.renderer.on = originalRendererOn; + + const originalUpdatePlaceholder = this.$updatePlaceholder; + + this.$updatePlaceholder = function () { + originalUpdatePlaceholder.call(this, ...arguments); + + if (this.renderer.placeholderNode) { + this.renderer.placeholderNode.innerHTML = this.$placeholder || ""; + } + }.bind(this); + + this.on("input", this.$updatePlaceholder); + } + + this.$updatePlaceholder(); + }; + }, }); diff --git a/app/assets/javascripts/admin/addon/components/admin-graph.js b/app/assets/javascripts/admin/addon/components/admin-graph.js index bee93aa1c5..b6bf870bc1 100644 --- a/app/assets/javascripts/admin/addon/components/admin-graph.js +++ b/app/assets/javascripts/admin/addon/components/admin-graph.js @@ -10,7 +10,7 @@ export default Component.extend({ const model = this.model; const rawData = this.get("model.data"); - var data = { + let data = { labels: rawData.map((r) => r.x), datasets: [ { diff --git a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js index ac073c4b55..ebf733e749 100644 --- a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js +++ b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js @@ -2,6 +2,7 @@ import Component from "@ember/component"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { fmt } from "discourse/lib/computed"; +import { isDocumentRTL } from "discourse/lib/text-direction"; import { next } from "@ember/runloop"; export default Component.extend({ @@ -43,9 +44,17 @@ export default Component.extend({ @discourseComputed("currentTargetName", "fieldName") placeholder(targetName, fieldName) { - return fieldName && fieldName === "color_definitions" - ? I18n.t("admin.customize.theme.color_definitions.placeholder") - : ""; + if (fieldName && fieldName === "color_definitions") { + const example = + ":root {\n" + + " --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};\n" + + "}"; + + return I18n.t("admin.customize.theme.color_definitions.placeholder", { + example: isDocumentRTL() ? `
${example}
` : example, + }); + } + return ""; }, @discourseComputed("fieldName", "currentTargetName", "theme") diff --git a/app/assets/javascripts/admin/addon/components/dashboard-new-features.js b/app/assets/javascripts/admin/addon/components/dashboard-new-features.js new file mode 100644 index 0000000000..937340c1d3 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/dashboard-new-features.js @@ -0,0 +1,26 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; +import { ajax } from "discourse/lib/ajax"; + +export default Component.extend({ + newFeatures: null, + releaseNotesLink: null, + + init() { + this._super(...arguments); + + ajax("/admin/dashboard/new-features.json").then((json) => { + this.setProperties({ + newFeatures: json.new_features, + releaseNotesLink: json.release_notes_link, + }); + }); + }, + + @action + dismissNewFeatures() { + ajax("/admin/dashboard/mark-new-features-as-seen.json", { + type: "PUT", + }).then(() => this.set("newFeatures", null)); + }, +}); diff --git a/app/assets/javascripts/admin/addon/components/secret-value-list.js b/app/assets/javascripts/admin/addon/components/secret-value-list.js index 5ad0e5b3ce..cd63e3b0ad 100644 --- a/app/assets/javascripts/admin/addon/components/secret-value-list.js +++ b/app/assets/javascripts/admin/addon/components/secret-value-list.js @@ -94,9 +94,9 @@ export default Component.extend({ _splitValues(values, delimiter) { if (values && values.length) { const keys = ["key", "secret"]; - var res = []; + let res = []; values.split(delimiter).forEach(function (str) { - var object = {}; + let object = {}; str.split("|").forEach(function (a, i) { object[keys[i]] = a; }); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js b/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js index 3653af5fb1..19f1d6ab6d 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js @@ -110,7 +110,7 @@ export default Controller.extend(bufferedProperty("model"), { const data = {}; const buffered = this.buffered; fields.forEach(function (field) { - var d = buffered.get(field); + let d = buffered.get(field); if (boolFields.includes(field)) { d = !!d; } diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js b/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js index 9b37e931da..bafcc02937 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js @@ -53,17 +53,18 @@ export default Controller.extend({ // Only confirm if we already been saved if (f.get("id")) { - bootbox.confirm(I18n.t("admin.user_fields.delete_confirm"), function ( - result - ) { - if (result) { - f.destroyRecord() - .then(function () { - model.removeObject(f); - }) - .catch(popupAjaxError); + bootbox.confirm( + I18n.t("admin.user_fields.delete_confirm"), + function (result) { + if (result) { + f.destroyRecord() + .then(function () { + model.removeObject(f); + }) + .catch(popupAjaxError); + } } - }); + ); } else { model.removeObject(f); } diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js index 81a42481e2..49e0da0d45 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js @@ -396,7 +396,6 @@ export default Controller.extend(CanCheckEmails, { destroy() { const postCount = this.get("model.post_count"); const maxPostCount = this.siteSettings.delete_all_posts_max; - const user = this.model; const message = I18n.t("admin.user.delete_confirm"); const location = document.location.pathname; @@ -422,13 +421,9 @@ export default Controller.extend(CanCheckEmails, { } } else { bootbox.alert(I18n.t("admin.user.delete_failed")); - if (data.user) { - user.setProperties(data.user); - } } }) .catch(() => { - AdminUser.find(user.get("id")).then((u) => user.setProperties(u)); bootbox.alert(I18n.t("admin.user.delete_failed")); }); }; @@ -601,8 +596,10 @@ export default Controller.extend(CanCheckEmails, { I18n.t("admin.user.sso.confirm_delete"), I18n.t("no_value"), I18n.t("yes_value"), - () => { - return this.model.deleteSSORecord(); + (confirmed) => { + if (confirmed) { + return this.model.deleteSSORecord(); + } } ); }, diff --git a/app/assets/javascripts/admin/addon/helpers/value-at-tl.js b/app/assets/javascripts/admin/addon/helpers/value-at-tl.js index 9e6b8c7514..158b02b546 100644 --- a/app/assets/javascripts/admin/addon/helpers/value-at-tl.js +++ b/app/assets/javascripts/admin/addon/helpers/value-at-tl.js @@ -1,9 +1,9 @@ import { registerUnbound } from "discourse-common/lib/helpers"; registerUnbound("value-at-tl", function (data, params) { - var tl = parseInt(params.level, 10); + let tl = parseInt(params.level, 10); if (data) { - var item = data.find(function (d) { + let item = data.find(function (d) { return parseInt(d.x, 10) === tl; }); if (item) { diff --git a/app/assets/javascripts/admin/addon/models/admin-dashboard.js b/app/assets/javascripts/admin/addon/models/admin-dashboard.js index 400ebe161b..c0c9f86702 100644 --- a/app/assets/javascripts/admin/addon/models/admin-dashboard.js +++ b/app/assets/javascripts/admin/addon/models/admin-dashboard.js @@ -14,6 +14,7 @@ AdminDashboard.reopenClass({ return ajax("/admin/dashboard.json").then((json) => { const model = AdminDashboard.create(); model.set("version_check", json.version_check); + return model; }); }, diff --git a/app/assets/javascripts/admin/addon/models/admin-user.js b/app/assets/javascripts/admin/addon/models/admin-user.js index 1a86426c5b..cbcdcc2e24 100644 --- a/app/assets/javascripts/admin/addon/models/admin-user.js +++ b/app/assets/javascripts/admin/addon/models/admin-user.js @@ -264,7 +264,17 @@ const AdminUser = User.extend({ return ajax(`/admin/users/${this.id}.json`, { type: "DELETE", data: formData, - }); + }) + .then((data) => { + if (!data.deleted && data.user) { + this.setProperties(data.user); + } + + return data; + }) + .catch(() => { + this.find(this.id).then((u) => this.setProperties(u)); + }); }, merge(formData) { diff --git a/app/assets/javascripts/admin/addon/routes/admin-user-badges.js b/app/assets/javascripts/admin/addon/routes/admin-user-badges.js index f9549e39e6..0c1621926f 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-user-badges.js +++ b/app/assets/javascripts/admin/addon/routes/admin-user-badges.js @@ -14,7 +14,7 @@ export default DiscourseRoute.extend({ Badge.findAll().then(function (badges) { controller.set("badges", badges); if (badges.length > 0) { - var grantableBadges = controller.get("grantableBadges"); + let grantableBadges = controller.get("grantableBadges"); if (grantableBadges.length > 0) { controller.set("selectedBadgeId", grantableBadges[0].get("id")); } diff --git a/app/assets/javascripts/admin/addon/services/admin-tools.js b/app/assets/javascripts/admin/addon/services/admin-tools.js index 16294be1b3..8928af14e5 100644 --- a/app/assets/javascripts/admin/addon/services/admin-tools.js +++ b/app/assets/javascripts/admin/addon/services/admin-tools.js @@ -33,8 +33,8 @@ export default Service.extend({ return AdminUser.find(userId).then((au) => this.spammerDetails(au)); }, - deleteUser(id) { - AdminUser.find(id).then((user) => user.destroy({ deletePosts: true })); + deleteUser(id, formData) { + return AdminUser.find(id).then((user) => user.destroy(formData)); }, spammerDetails(adminUser) { diff --git a/app/assets/javascripts/admin/addon/templates/badges-show.hbs b/app/assets/javascripts/admin/addon/templates/badges-show.hbs index 82057b103c..429f0cbc94 100644 --- a/app/assets/javascripts/admin/addon/templates/badges-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/badges-show.hbs @@ -5,7 +5,7 @@ {{#if readOnly}} {{input type="text" name="name" value=buffered.name disabled=true}}

- {{#link-to "adminSiteText.edit" (concat textCustomizationPrefix "name")}} + {{#link-to "adminSiteText" (query-params q=(concat textCustomizationPrefix "name"))}} {{i18n "admin.badges.read_only_setting_help"}} {{/link-to}}

@@ -70,7 +70,7 @@ {{#if buffered.system}} {{textarea name="description" value=buffered.description disabled=true}}

- {{#link-to "adminSiteText.edit" (concat textCustomizationPrefix "description")}} + {{#link-to "adminSiteText" (query-params q=(concat textCustomizationPrefix "description"))}} {{i18n "admin.badges.read_only_setting_help"}} {{/link-to}}

@@ -84,7 +84,7 @@ {{#if buffered.system}} {{textarea name="long_description" value=buffered.long_description disabled=true}}

- {{#link-to "adminSiteText.edit" (concat textCustomizationPrefix "long_description")}} + {{#link-to "adminSiteText" (query-params q=(concat textCustomizationPrefix "long_description"))}} {{i18n "admin.badges.read_only_setting_help"}} {{/link-to}}

diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs index ce928108a2..5458ee10d8 100644 --- a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs @@ -87,4 +87,4 @@
{{error}}
{{/if}} -{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true" placeholder=placeholder}} +{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true" placeholder=placeholder htmlPlaceholder=true}} diff --git a/app/assets/javascripts/admin/addon/templates/components/dashboard-new-feature-item.hbs b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-feature-item.hbs new file mode 100644 index 0000000000..2aff298640 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-feature-item.hbs @@ -0,0 +1,13 @@ +
+
{{item.emoji}}
+
+
+ {{#if item.link}} + {{item.title}} + {{else}} + {{item.title}} + {{/if}} +
+
{{item.description}}
+
+
diff --git a/app/assets/javascripts/admin/addon/templates/components/dashboard-new-features.hbs b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-features.hbs new file mode 100644 index 0000000000..190e53a432 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-features.hbs @@ -0,0 +1,21 @@ +{{#if newFeatures}} +
+
+

{{replace-emoji (i18n "admin.dashboard.new_features.title") }}

+
+ +
+ {{#each newFeatures as |feature|}} + {{dashboard-new-feature-item item=feature}} + {{/each}} +
+ +
+{{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/dashboard.hbs b/app/assets/javascripts/admin/addon/templates/dashboard.hbs index 0e4f4beec2..cb87e84b47 100644 --- a/app/assets/javascripts/admin/addon/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/addon/templates/dashboard.hbs @@ -1,3 +1,5 @@ +{{dashboard-new-features}} + {{plugin-outlet name="admin-dashboard-top"}} {{#if showVersionChecks}} diff --git a/app/assets/javascripts/browser-update.js b/app/assets/javascripts/browser-update.js index cd2f60430b..405b2e4a1d 100644 --- a/app/assets/javascripts/browser-update.js +++ b/app/assets/javascripts/browser-update.js @@ -2,10 +2,10 @@ //Copyright (c) 2007-2009, MIT Style License (function () { - var $buo = function () { + let $buo = function () { // Sometimes we have to resort to parsing the user agent string. :( if (navigator && navigator.userAgent) { - var ua = navigator.userAgent; + let ua = navigator.userAgent; // we don't ask Googlebot to update their browser if ( @@ -22,10 +22,10 @@ } document.getElementsByTagName("body")[0].className += " crawler"; - var mainElement = document.getElementById("main"); - var noscriptElements = document.getElementsByTagName("noscript"); + let mainElement = document.getElementById("main"); + let noscriptElements = document.getElementsByTagName("noscript"); // find the element with the "data-path" attribute set - for (var i = 0; i < noscriptElements.length; ++i) { + for (let i = 0; i < noscriptElements.length; ++i) { if (noscriptElements[i].getAttribute("data-path")) { // noscriptElements[i].innerHTML contains encoded HTML if (noscriptElements[i].childNodes.length > 0) { @@ -36,7 +36,7 @@ } // retrieve localized browser upgrade text - var t = I18n.t("browser_update"); // eslint-disable-line no-undef + let t = I18n.t("browser_update"); // eslint-disable-line no-undef if (t.indexOf(".browser_update]") !== -1) { // very old browsers might fail to load even translations t = @@ -44,13 +44,13 @@ } // create the notification div HTML - var div = document.createElement("div"); + let div = document.createElement("div"); div.className = "buorg"; div.innerHTML = "
" + t + "
"; // create the notification div stylesheet - var sheet = document.createElement("style"); - var style = + let sheet = document.createElement("style"); + let style = ".buorg {position:absolute; z-index:111111; width:100%; top:0px; left:0px; background:#FDF2AB; text-align:left; font-family: sans-serif; color:#000; font-size: 14px;} .buorg div {padding: 8px;} .buorg a, .buorg a:visited {color:#E25600; text-decoration: underline;} @media print { .buorg { display: none !important; } }"; // insert the div and stylesheet into the DOM diff --git a/app/assets/javascripts/discourse-common/addon/config/environment.js b/app/assets/javascripts/discourse-common/addon/config/environment.js index 409b6af066..d2a47b1247 100644 --- a/app/assets/javascripts/discourse-common/addon/config/environment.js +++ b/app/assets/javascripts/discourse-common/addon/config/environment.js @@ -14,6 +14,12 @@ export function isTesting() { return Ember.testing || environment === "testing"; } +// Generally means "before we migrated to Ember CLI" +let _isLegacy = Ember.VERSION.startsWith("3.12"); +export function isLegacyEmber() { + return _isLegacy; +} + export function isDevelopment() { return environment === "development"; } diff --git a/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js b/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js index 07bda95a9e..d60b94eaf7 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js +++ b/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js @@ -32,8 +32,8 @@ AttributeHook.prototype.unhook = function (node, prop, next) { return; } - var colonPosition = prop.indexOf(":"); - var localName = colonPosition > -1 ? prop.substr(colonPosition + 1) : prop; + let colonPosition = prop.indexOf(":"); + let localName = colonPosition > -1 ? prop.substr(colonPosition + 1) : prop; node.removeAttributeNS(this.namespace, localName); }; diff --git a/app/assets/javascripts/discourse-common/addon/lib/debounce.js b/app/assets/javascripts/discourse-common/addon/lib/debounce.js index 541498fda6..5e30864574 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/debounce.js +++ b/app/assets/javascripts/discourse-common/addon/lib/debounce.js @@ -1,14 +1,17 @@ -import { debounce, run } from "@ember/runloop"; -import { isTesting } from "discourse-common/config/environment"; +import { debounce, next, run } from "@ember/runloop"; +import { isLegacyEmber, isTesting } from "discourse-common/config/environment"; + /** Debounce a Javascript function. This means if it's called many times in a time limit it should only be executed once (at the end of the limit counted from the last call made). Original function will be called with the context and arguments from the last call made. **/ +let testingFunc = isLegacyEmber() ? run : next; + export default function () { if (isTesting()) { - return run(...arguments); + return testingFunc(...arguments); } else { return debounce(...arguments); } diff --git a/app/assets/javascripts/discourse-common/addon/lib/get-url.js b/app/assets/javascripts/discourse-common/addon/lib/get-url.js index 1981a38bfb..29a9e56ce1 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/get-url.js +++ b/app/assets/javascripts/discourse-common/addon/lib/get-url.js @@ -15,9 +15,9 @@ export default function getURL(url) { return url; } - const found = url.indexOf(baseUri); + const found = url.startsWith(baseUri); - if (found >= 0 && found < 3) { + if (found) { return url; } if (url[0] !== "/") { diff --git a/app/assets/javascripts/discourse-common/addon/lib/helpers.js b/app/assets/javascripts/discourse-common/addon/lib/helpers.js index e9af233da8..a2014653c8 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/helpers.js +++ b/app/assets/javascripts/discourse-common/addon/lib/helpers.js @@ -22,7 +22,7 @@ const _helpers = {}; function rawGet(ctx, property, options) { if (options.types && options.data.view) { - var view = options.data.view; + let view = options.data.view; return view.getStream ? view.getStream(property).value() : view.getAttr(property); diff --git a/app/assets/javascripts/discourse-common/addon/lib/object.js b/app/assets/javascripts/discourse-common/addon/lib/object.js index 4fe99d96b8..294235b1ce 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/object.js +++ b/app/assets/javascripts/discourse-common/addon/lib/object.js @@ -47,7 +47,7 @@ export function deepEqual(obj1, obj2) { if (Object.keys(obj1).length !== Object.keys(obj2).length) { return false; } - for (var prop in obj1) { + for (let prop in obj1) { if (!deepEqual(obj1[prop], obj2[prop])) { return false; } diff --git a/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js b/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js index 37bb2c93d0..634d77c39e 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js +++ b/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js @@ -29,24 +29,22 @@ export function registerRawHelpers(hbs, handlebarsClass) { }; // #each .. in support (as format is transformed to this) - hbs.registerHelper("each", function ( - localName, - inKeyword, - contextName, - options - ) { - if (typeof contextName === "undefined") { - return; + hbs.registerHelper( + "each", + function (localName, inKeyword, contextName, options) { + if (typeof contextName === "undefined") { + return; + } + let list = get(this, contextName); + let output = []; + for (let i = 0; i < list.length; i++) { + let innerContext = {}; + innerContext[localName] = list[i]; + output.push(options.fn(innerContext)); + } + return output.join(""); } - var list = get(this, contextName); - var output = []; - for (var i = 0; i < list.length; i++) { - let innerContext = {}; - innerContext[localName] = list[i]; - output.push(options.fn(innerContext)); - } - return output.join(""); - }); + ); function stringCompatHelper(fn) { const old = hbs.helpers[fn]; diff --git a/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js b/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js index d2d3149c79..f49e427317 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js +++ b/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js @@ -6,7 +6,7 @@ import Handlebars from "handlebars"; const RawHandlebars = Handlebars.create(); function buildPath(blk, args) { - var result = { + let result = { type: "PathExpression", data: false, depth: blk.path.depth, @@ -22,7 +22,7 @@ function buildPath(blk, args) { } function replaceGet(ast) { - var visitor = new Handlebars.Visitor(); + let visitor = new Handlebars.Visitor(); visitor.mutating = true; visitor.MustacheStatement = function (mustache) { @@ -42,7 +42,7 @@ function replaceGet(ast) { // This allows us to use the same syntax in all templates visitor.BlockStatement = function (block) { if (block.path.original === "each" && block.params.length === 1) { - var paramName = block.program.blockParams[0]; + let paramName = block.program.blockParams[0]; block.params = [ buildPath(block, { original: paramName }), { type: "CommentStatement", value: "in" }, @@ -74,10 +74,10 @@ if (Handlebars.Compiler) { RawHandlebars.JavaScriptCompiler.prototype.namespace = "RawHandlebars"; RawHandlebars.precompile = function (value, asObject) { - var ast = Handlebars.parse(value); + let ast = Handlebars.parse(value); replaceGet(ast); - var options = { + let options = { knownHelpers: { get: true, }, @@ -87,7 +87,7 @@ if (Handlebars.Compiler) { asObject = asObject === undefined ? true : asObject; - var environment = new RawHandlebars.Compiler().compile(ast, options); + let environment = new RawHandlebars.Compiler().compile(ast, options); return new RawHandlebars.JavaScriptCompiler().compile( environment, options, @@ -97,20 +97,20 @@ if (Handlebars.Compiler) { }; RawHandlebars.compile = function (string) { - var ast = Handlebars.parse(string); + let ast = Handlebars.parse(string); replaceGet(ast); // this forces us to rewrite helpers - var options = { data: true, stringParams: true }; - var environment = new RawHandlebars.Compiler().compile(ast, options); - var templateSpec = new RawHandlebars.JavaScriptCompiler().compile( + let options = { data: true, stringParams: true }; + let environment = new RawHandlebars.Compiler().compile(ast, options); + let templateSpec = new RawHandlebars.JavaScriptCompiler().compile( environment, options, undefined, true ); - var t = RawHandlebars.template(templateSpec); + let t = RawHandlebars.template(templateSpec); t.isMethod = false; return t; diff --git a/app/assets/javascripts/discourse-common/addon/resolver.js b/app/assets/javascripts/discourse-common/addon/resolver.js index f720febb29..51222877ff 100644 --- a/app/assets/javascripts/discourse-common/addon/resolver.js +++ b/app/assets/javascripts/discourse-common/addon/resolver.js @@ -111,7 +111,7 @@ export function buildResolver(baseName) { ); }); - var module; + let module; if (moduleName) { module = requirejs(moduleName, null, null, true /* force sync */); if (module && module["default"]) { @@ -200,7 +200,7 @@ export function buildResolver(baseName) { findPluginMobileTemplate(parsedName) { if (_options.mobileView) { - var pluginParsedName = this.parseName( + let pluginParsedName = this.parseName( parsedName.fullName.replace( "template:", "template:javascripts/mobile/" @@ -212,7 +212,7 @@ export function buildResolver(baseName) { findMobileTemplate(parsedName) { if (_options.mobileView) { - var mobileParsedName = this.parseName( + let mobileParsedName = this.parseName( parsedName.fullName.replace("template:", "template:mobile/") ); return this.findTemplate(mobileParsedName); @@ -241,15 +241,15 @@ export function buildResolver(baseName) { }, findUnderscoredTemplate(parsedName) { - var decamelized = parsedName.fullNameWithoutType.decamelize(); - var underscored = decamelized.replace(/\-/g, "_"); + let decamelized = parsedName.fullNameWithoutType.decamelize(); + let underscored = decamelized.replace(/\-/g, "_"); return Ember.TEMPLATES[underscored]; }, // Try to find a template within a special admin namespace, e.g. adminEmail => admin/templates/email // (similar to how discourse lays out templates) findAdminTemplate(parsedName) { - var decamelized = parsedName.fullNameWithoutType.decamelize(); + let decamelized = parsedName.fullNameWithoutType.decamelize(); if (decamelized.indexOf("components") === 0) { let comPath = `admin/templates/${decamelized}`; const compTemplate = diff --git a/app/assets/javascripts/discourse-common/addon/utils/decorator-alias.js b/app/assets/javascripts/discourse-common/addon/utils/decorator-alias.js index 96692bb553..dd4299c700 100644 --- a/app/assets/javascripts/discourse-common/addon/utils/decorator-alias.js +++ b/app/assets/javascripts/discourse-common/addon/utils/decorator-alias.js @@ -12,7 +12,7 @@ export default function decoratorAlias(fn, errorMessage) { configurable: desc.configurable, writable: desc.writable, initializer: function () { - var value = extractValue(desc); + let value = extractValue(desc); return fn.apply(null, params.concat(value)); }, }; diff --git a/app/assets/javascripts/discourse-common/addon/utils/decorators.js b/app/assets/javascripts/discourse-common/addon/utils/decorators.js index c7f3729d8e..4fad373bf8 100644 --- a/app/assets/javascripts/discourse-common/addon/utils/decorators.js +++ b/app/assets/javascripts/discourse-common/addon/utils/decorators.js @@ -51,7 +51,7 @@ export function readOnly(target, name, desc) { enumerable: desc.enumerable, configurable: desc.configurable, initializer: function () { - var value = extractValue(desc); + let value = extractValue(desc); return value.readOnly(); }, }; diff --git a/app/assets/javascripts/discourse-common/addon/utils/handle-descriptor.js b/app/assets/javascripts/discourse-common/addon/utils/handle-descriptor.js index f8ff22c817..3987adffa6 100644 --- a/app/assets/javascripts/discourse-common/addon/utils/handle-descriptor.js +++ b/app/assets/javascripts/discourse-common/addon/utils/handle-descriptor.js @@ -10,7 +10,7 @@ export default function handleDescriptor(target, key, desc, params = []) { let computedDescriptor; if (desc.writable) { - var val = extractValue(desc); + let val = extractValue(desc); if (typeof val === "object") { let value = {}; if (val.get) { diff --git a/app/assets/javascripts/discourse-hbr/raw-handlebars-compiler.js b/app/assets/javascripts/discourse-hbr/raw-handlebars-compiler.js index 1cbffd611b..659f924114 100644 --- a/app/assets/javascripts/discourse-hbr/raw-handlebars-compiler.js +++ b/app/assets/javascripts/discourse-hbr/raw-handlebars-compiler.js @@ -6,7 +6,7 @@ const Handlebars = require("handlebars"); const RawHandlebars = Handlebars.create(); function buildPath(blk, args) { - var result = { + let result = { type: "PathExpression", data: false, depth: blk.path.depth, @@ -22,7 +22,7 @@ function buildPath(blk, args) { } function replaceGet(ast) { - var visitor = new Handlebars.Visitor(); + let visitor = new Handlebars.Visitor(); visitor.mutating = true; visitor.MustacheStatement = function (mustache) { @@ -42,7 +42,7 @@ function replaceGet(ast) { // This allows us to use the same syntax in all templates visitor.BlockStatement = function (block) { if (block.path.original === "each" && block.params.length === 1) { - var paramName = block.program.blockParams[0]; + let paramName = block.program.blockParams[0]; block.params = [ buildPath(block, { original: paramName }), { type: "CommentStatement", value: "in" }, @@ -71,10 +71,10 @@ RawHandlebars.JavaScriptCompiler.prototype.compiler = RawHandlebars.JavaScriptCompiler.prototype.namespace = "RawHandlebars"; RawHandlebars.precompile = function (value, asObject) { - var ast = Handlebars.parse(value); + let ast = Handlebars.parse(value); replaceGet(ast); - var options = { + let options = { knownHelpers: { get: true, }, @@ -84,7 +84,7 @@ RawHandlebars.precompile = function (value, asObject) { asObject = asObject === undefined ? true : asObject; - var environment = new RawHandlebars.Compiler().compile(ast, options); + let environment = new RawHandlebars.Compiler().compile(ast, options); return new RawHandlebars.JavaScriptCompiler().compile( environment, options, @@ -94,20 +94,20 @@ RawHandlebars.precompile = function (value, asObject) { }; RawHandlebars.compile = function (string) { - var ast = Handlebars.parse(string); + let ast = Handlebars.parse(string); replaceGet(ast); // this forces us to rewrite helpers - var options = { data: true, stringParams: true }; - var environment = new RawHandlebars.Compiler().compile(ast, options); - var templateSpec = new RawHandlebars.JavaScriptCompiler().compile( + let options = { data: true, stringParams: true }; + let environment = new RawHandlebars.Compiler().compile(ast, options); + let templateSpec = new RawHandlebars.JavaScriptCompiler().compile( environment, options, undefined, true ); - var t = RawHandlebars.template(templateSpec); + let t = RawHandlebars.template(templateSpec); t.isMethod = false; return t; diff --git a/app/assets/javascripts/discourse/app/app.js b/app/assets/javascripts/discourse/app/app.js index 093dfe5eea..b28effdddb 100644 --- a/app/assets/javascripts/discourse/app/app.js +++ b/app/assets/javascripts/discourse/app/app.js @@ -26,7 +26,8 @@ const Discourse = Application.extend({ const init = module.default; const oldInitialize = init.initialize; - init.initialize = () => oldInitialize.call(init, this.__container__, this); + init.initialize = (app) => oldInitialize.call(init, app.__container__, app); + return init; }, diff --git a/app/assets/javascripts/discourse/app/components/activation-controls.js b/app/assets/javascripts/discourse/app/components/activation-controls.js index 15d48468b0..99ed8d2b1b 100644 --- a/app/assets/javascripts/discourse/app/components/activation-controls.js +++ b/app/assets/javascripts/discourse/app/components/activation-controls.js @@ -1,4 +1,10 @@ import Component from "@ember/component"; +import { or } from "@ember/object/computed"; + export default Component.extend({ classNames: "activation-controls", + canEditEmail: or( + "siteSettings.enable_local_logins", + "siteSettings.email_editable" + ), }); diff --git a/app/assets/javascripts/discourse/app/components/backup-codes.js b/app/assets/javascripts/discourse/app/components/backup-codes.js index e66fe6de63..565f282174 100644 --- a/app/assets/javascripts/discourse/app/components/backup-codes.js +++ b/app/assets/javascripts/discourse/app/components/backup-codes.js @@ -5,12 +5,12 @@ import discourseComputed from "discourse-common/utils/decorators"; // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding function b64EncodeUnicode(str) { return btoa( - encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function toSolidBytes( - match, - p1 - ) { - return String.fromCharCode("0x" + p1); - }) + encodeURIComponent(str).replace( + /%([0-9A-F]{2})/g, + function toSolidBytes(match, p1) { + return String.fromCharCode("0x" + p1); + } + ) ); } diff --git a/app/assets/javascripts/discourse/app/components/basic-topic-list.js b/app/assets/javascripts/discourse/app/components/basic-topic-list.js index 0445741f0b..3343512568 100644 --- a/app/assets/javascripts/discourse/app/components/basic-topic-list.js +++ b/app/assets/javascripts/discourse/app/components/basic-topic-list.js @@ -10,7 +10,7 @@ export default Component.extend({ @discourseComputed("topicList.loaded") loaded() { - var topicList = this.topicList; + let topicList = this.topicList; if (topicList) { return topicList.get("loaded"); } else { diff --git a/app/assets/javascripts/discourse/app/components/bookmark-local-date.js b/app/assets/javascripts/discourse/app/components/bookmark-local-date.js new file mode 100644 index 0000000000..d89c200073 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bookmark-local-date.js @@ -0,0 +1,57 @@ +import Component from "@ember/component"; +import I18n from "I18n"; +import { action } from "@ember/object"; +import { getOwner } from "discourse-common/lib/get-owner"; +import { or } from "@ember/object/computed"; + +export default Component.extend({ + tagName: "", + + init() { + this._super(...arguments); + + this.loadLocalDates(); + }, + + get postLocalDateFormatted() { + return this.postLocalDate().format(I18n.t("dates.long_no_year")); + }, + + showPostLocalDate: or("postDetectedLocalDate", "postDetectedLocalTime"), + + loadLocalDates() { + let postEl = document.querySelector(`[data-post-id="${this.postId}"]`); + let localDateEl = null; + if (postEl) { + localDateEl = postEl.querySelector(".discourse-local-date"); + } + + this.setProperties({ + postDetectedLocalDate: localDateEl ? localDateEl.dataset.date : null, + postDetectedLocalTime: localDateEl ? localDateEl.dataset.time : null, + postDetectedLocalTimezone: localDateEl + ? localDateEl.dataset.timezone + : null, + }); + }, + + postLocalDate() { + const bookmarkController = getOwner(this).lookup("controller:bookmark"); + let parsedPostLocalDate = bookmarkController._parseCustomDateTime( + this.postDetectedLocalDate, + this.postDetectedLocalTime, + this.postDetectedLocalTimezone + ); + + if (!this.postDetectedLocalTime) { + return bookmarkController.startOfDay(parsedPostLocalDate); + } + + return parsedPostLocalDate; + }, + + @action + setReminder() { + return this.onChange(this.postLocalDate()); + }, +}); diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index 2ed3a8712a..b666a5ad33 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -536,10 +536,10 @@ export default Component.extend({ _warnMentionedGroups($preview) { schedule("afterRender", () => { - var found = this.warnedGroupMentions || []; + let found = this.warnedGroupMentions || []; $preview.find(".mention-group.notify").each((idx, e) => { const $e = $(e); - var name = $e.data("name"); + let name = $e.data("name"); if (found.indexOf(name) === -1) { this.groupsMentioned([ { diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index c5c636ab67..de6531d172 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -184,7 +184,7 @@ class Toolbar { if (button.shortcut) { const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); const mod = mac ? "Meta" : "Ctrl"; - var shortcutTitle = `${mod}+${button.shortcut}`; + let shortcutTitle = `${mod}+${button.shortcut}`; // Mac users are used to glyphs for shortcut keys if (mac) { diff --git a/app/assets/javascripts/discourse/app/components/d-modal.js b/app/assets/javascripts/discourse/app/components/d-modal.js index 2ef23e5a17..e74f12d566 100644 --- a/app/assets/javascripts/discourse/app/components/d-modal.js +++ b/app/assets/javascripts/discourse/app/components/d-modal.js @@ -1,3 +1,4 @@ +import { computed } from "@ember/object"; import Component from "@ember/component"; import I18n from "I18n"; import afterTransition from "discourse/lib/after-transition"; @@ -12,10 +13,16 @@ export default Component.extend({ "modalStyle", "hasPanels", ], - attributeBindings: ["data-keyboard", "aria-modal"], + attributeBindings: [ + "data-keyboard", + "aria-modal", + "role", + "ariaLabelledby:aria-labelledby", + ], dismissable: true, title: null, subtitle: null, + role: "dialog", init() { this._super(...arguments); @@ -33,6 +40,10 @@ export default Component.extend({ // Inform screenreaders of the modal "aria-modal": "true", + ariaLabelledby: computed("title", function () { + return this.title ? "discourse-modal-title" : null; + }), + @on("didInsertElement") setUp() { $("html").on("keyup.discourse-modal", (e) => { diff --git a/app/assets/javascripts/discourse/app/components/popup-input-tip.js b/app/assets/javascripts/discourse/app/components/popup-input-tip.js index 51d5519862..9d02aeff01 100644 --- a/app/assets/javascripts/discourse/app/components/popup-input-tip.js +++ b/app/assets/javascripts/discourse/app/components/popup-input-tip.js @@ -28,7 +28,7 @@ export default Component.extend({ @observes("lastShownAt") bounce() { if (this.lastShownAt) { - var $elem = $(this.element); + let $elem = $(this.element); if (!this.animateAttribute) { this.animateAttribute = $elem.css("left") === "auto" ? "right" : "left"; } @@ -51,7 +51,7 @@ export default Component.extend({ }, bounceLeft($elem) { - for (var i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { $elem .animate({ left: "+=" + this.bouncePixels }, this.bounceDelay) .animate({ left: "-=" + this.bouncePixels }, this.bounceDelay); @@ -59,7 +59,7 @@ export default Component.extend({ }, bounceRight($elem) { - for (var i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { $elem .animate({ right: "-=" + this.bouncePixels }, this.bounceDelay) .animate({ right: "+=" + this.bouncePixels }, this.bounceDelay); diff --git a/app/assets/javascripts/discourse/app/components/share-popup.js b/app/assets/javascripts/discourse/app/components/share-popup.js index 3b2c3eb093..a2d9decc13 100644 --- a/app/assets/javascripts/discourse/app/components/share-popup.js +++ b/app/assets/javascripts/discourse/app/components/share-popup.js @@ -56,7 +56,7 @@ export default Component.extend({ }, _showUrl($target, url) { - const $currentTargetOffset = $target.offset(); + const currentTargetOffset = $target.offset(); const $this = $(this.element); if (isEmpty(url)) { @@ -69,7 +69,7 @@ export default Component.extend({ } const shareLinkWidth = $this.width(); - let x = $currentTargetOffset.left - shareLinkWidth / 2; + let x = currentTargetOffset.left - shareLinkWidth / 2; if (x < 25) { x = 25; } @@ -78,15 +78,18 @@ export default Component.extend({ } const header = $(".d-header"); - let y = $currentTargetOffset.top - ($this.height() + 20); + let y = currentTargetOffset.top - ($this.height() + 20); if (y < header.offset().top + header.height()) { - y = $currentTargetOffset.top + 10; + y = currentTargetOffset.top + 10; } - $this.css({ top: "" + y + "px" }); + this.element.style.top = `${y}px`; if (!this.site.mobileView) { - $this.css({ left: "" + x + "px" }); + this.element.style.left = `${x}px`; + if (document.documentElement.classList.contains("rtl")) { + this.element.style.right = "unset"; + } } this.set("link", url); this.set("visible", true); diff --git a/app/assets/javascripts/discourse/app/components/tag-groups-form.js b/app/assets/javascripts/discourse/app/components/tag-groups-form.js index d9ba0fff3b..d701fb300c 100644 --- a/app/assets/javascripts/discourse/app/components/tag-groups-form.js +++ b/app/assets/javascripts/discourse/app/components/tag-groups-form.js @@ -107,6 +107,8 @@ export default Component.extend(bufferedProperty("model"), { this.allGroups.forEach((group) => { if (groupIds.includes(group.id)) { updatedPermissions[group.name] = PermissionType.FULL; + } else { + delete updatedPermissions[group.name]; } }); diff --git a/app/assets/javascripts/discourse/app/components/tap-tile.js b/app/assets/javascripts/discourse/app/components/tap-tile.js index b384ff7d71..8671cc1ad2 100644 --- a/app/assets/javascripts/discourse/app/components/tap-tile.js +++ b/app/assets/javascripts/discourse/app/components/tap-tile.js @@ -1,13 +1,26 @@ +import { reads } from "@ember/object/computed"; import Component from "@ember/component"; import { propertyEqual } from "discourse/lib/computed"; export default Component.extend({ init() { this._super(...arguments); + this.set("elementId", `tap_tile_${this.tileId}`); }, + classNames: ["tap-tile"], + classNameBindings: ["active"], + + attributeBindings: ["role", "ariaPressed", "tabIndex"], + + role: "button", + + tabIndex: 0, + + ariaPressed: reads("active"), + click() { this.onChange(this.tileId); }, diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index 22d67f2d00..2efef6a1f3 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -226,12 +226,6 @@ export default Component.extend({ return this.unhandledRowClick(e, topic); }, - actions: { - toggleBookmark() { - this.topic.toggleBookmark().finally(() => this.renderTopicListItem()); - }, - }, - unhandledRowClick() {}, navigateToTopic, diff --git a/app/assets/javascripts/discourse/app/components/topic-list.js b/app/assets/javascripts/discourse/app/components/topic-list.js index 2f1270844f..8383cb035e 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list.js +++ b/app/assets/javascripts/discourse/app/components/topic-list.js @@ -163,9 +163,9 @@ export default Component.extend(LoadMore, { }, click(e) { - var self = this; - var onClick = function (sel, callback) { - var target = $(e.target).closest(sel); + let self = this; + let onClick = function (sel, callback) { + let target = $(e.target).closest(sel); if (target.length === 1) { callback.apply(self, [target]); diff --git a/app/assets/javascripts/discourse/app/components/user-badge.js b/app/assets/javascripts/discourse/app/components/user-badge.js index 201fa55021..6a2c8ceddb 100644 --- a/app/assets/javascripts/discourse/app/components/user-badge.js +++ b/app/assets/javascripts/discourse/app/components/user-badge.js @@ -12,7 +12,7 @@ export default Component.extend({ @discourseComputed("badge", "user") badgeUrl() { // NOTE: I tried using a link-to helper here but the queryParams mean it fails - var username = this.get("user.username_lower") || ""; + let username = this.get("user.username_lower") || ""; username = username !== "" ? "?username=" + username : ""; return this.get("badge.url") + username; }, diff --git a/app/assets/javascripts/discourse/app/controllers/bookmark.js b/app/assets/javascripts/discourse/app/controllers/bookmark.js index 5cab58bd0a..ec0e0010bb 100644 --- a/app/assets/javascripts/discourse/app/controllers/bookmark.js +++ b/app/assets/javascripts/discourse/app/controllers/bookmark.js @@ -1,5 +1,4 @@ import { REMINDER_TYPES, formattedReminderTime } from "discourse/lib/bookmark"; -import { and, or } from "@ember/object/computed"; import { isEmpty, isPresent } from "@ember/utils"; import { next, schedule } from "@ember/runloop"; import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark"; @@ -10,6 +9,7 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; import { Promise } from "rsvp"; import { action } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; +import { and } from "@ember/object/computed"; import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -62,9 +62,7 @@ export default Controller.extend(ModalFunctionality, { customReminderTime: null, lastCustomReminderDate: null, lastCustomReminderTime: null, - postDetectedLocalDate: null, - postDetectedLocalTime: null, - postDetectedLocalTimezone: null, + postLocalDate: null, mouseTrap: null, userTimezone: null, showOptions: false, @@ -95,6 +93,8 @@ export default Controller.extend(ModalFunctionality, { this._initializeExistingBookmarkData(); } + this.loadLocalDates(); + schedule("afterRender", () => { if (this.site.isMobileDevice) { document.getElementById("bookmark-name").blur(); @@ -240,11 +240,6 @@ export default Controller.extend(ModalFunctionality, { showLastCustom: and("lastCustomReminderTime", "lastCustomReminderDate"), - showPostLocalDate: or( - "model.postDetectedLocalDate", - "model.postDetectedLocalTime" - ), - get showLaterToday() { let later = this.laterToday(); return ( @@ -302,8 +297,22 @@ export default Controller.extend(ModalFunctionality, { return this.nextMonth().format(I18n.t("dates.long_no_year")); }, - get postLocalDateFormatted() { - return this.postLocalDate().format(I18n.t("dates.long_no_year")); + loadLocalDates() { + let postEl = document.querySelector( + `[data-post-id="${this.model.postId}"]` + ); + let localDateEl = null; + if (postEl) { + localDateEl = postEl.querySelector(".discourse-local-date"); + } + + if (localDateEl) { + this.setProperties({ + postDetectedLocalDate: localDateEl.dataset.date, + postDetectedLocalTime: localDateEl.dataset.time, + postDetectedLocalTimezone: localDateEl.dataset.timezone, + }); + } }, @discourseComputed("userTimezone") @@ -442,7 +451,7 @@ export default Controller.extend(ModalFunctionality, { case REMINDER_TYPES.LAST_CUSTOM: return this.parsedLastCustomReminderDatetime; case REMINDER_TYPES.POST_LOCAL_DATE: - return this.postLocalDate(); + return this.postLocalDate; } }, @@ -454,20 +463,6 @@ export default Controller.extend(ModalFunctionality, { return this.startOfDay(this.now().add(1, "month")); }, - postLocalDate() { - let parsedPostLocalDate = this._parseCustomDateTime( - this.model.postDetectedLocalDate, - this.model.postDetectedLocalTime, - this.model.postDetectedLocalTimezone - ); - - if (!this.model.postDetectedLocalTime) { - return this.startOfDay(parsedPostLocalDate); - } - - return parsedPostLocalDate; - }, - tomorrow() { return this.startOfDay(this.now().add(1, "day")); }, @@ -572,4 +567,13 @@ export default Controller.extend(ModalFunctionality, { return this.saveAndClose(); } }, + + @action + selectPostLocalDate(date) { + this.setProperties({ + selectedReminderType: this.reminderTypes.POST_LOCAL_DATE, + postLocalDate: date, + }); + return this.saveAndClose(); + }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index afdf82bb26..6f8d6e245e 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -727,7 +727,7 @@ export default Controller.extend({ } } - var staged = false; + let staged = false; // TODO: This should not happen in model const imageSizes = {}; diff --git a/app/assets/javascripts/discourse/app/controllers/discovery/categories.js b/app/assets/javascripts/discourse/app/controllers/discovery/categories.js index 01879ddc2e..b286fd4fda 100644 --- a/app/assets/javascripts/discourse/app/controllers/discovery/categories.js +++ b/app/assets/javascripts/discourse/app/controllers/discovery/categories.js @@ -19,15 +19,6 @@ export default DiscoveryController.extend({ canEdit: reads("currentUser.staff"), - @discourseComputed("model.categories.[].featuredTopics.length") - latestTopicOnly() { - return ( - this.get("model.categories").find( - (c) => c.get("featuredTopics.length") > 1 - ) === undefined - ); - }, - @discourseComputed("model.parentCategory") categoryPageStyle(parentCategory) { let style = this.site.mobileView diff --git a/app/assets/javascripts/discourse/app/controllers/edit-category-tabs.js b/app/assets/javascripts/discourse/app/controllers/edit-category-tabs.js index 9b4aad2586..bbfa66193d 100644 --- a/app/assets/javascripts/discourse/app/controllers/edit-category-tabs.js +++ b/app/assets/javascripts/discourse/app/controllers/edit-category-tabs.js @@ -1,3 +1,4 @@ +import { and, readOnly } from "@ember/object/computed"; import discourseComputed, { on } from "discourse-common/utils/decorators"; import Category from "discourse/models/category"; import Controller from "@ember/controller"; @@ -7,7 +8,6 @@ import { NotificationLevels } from "discourse/lib/notification-levels"; import PermissionType from "discourse/models/permission-type"; import bootbox from "bootbox"; import { extractError } from "discourse/lib/ajax-error"; -import { readOnly } from "@ember/object/computed"; import { underscore } from "@ember/string"; export default Controller.extend({ @@ -15,11 +15,12 @@ export default Controller.extend({ saving: false, deleting: false, panels: null, - hiddenTooltip: true, + showTooltip: false, createdCategory: false, expandedMenu: false, mobileView: readOnly("site.mobileView"), parentParams: null, + showDeleteReason: and("showTooltip", "model.cannot_delete_reason"), @on("init") _initPanels() { @@ -143,7 +144,7 @@ export default Controller.extend({ }, toggleDeleteTooltip() { - this.toggleProperty("hiddenTooltip"); + this.toggleProperty("showTooltip"); }, goBack() { diff --git a/app/assets/javascripts/discourse/app/controllers/full-page-search.js b/app/assets/javascripts/discourse/app/controllers/full-page-search.js index 44dee80ae3..a7ba222f8c 100644 --- a/app/assets/javascripts/discourse/app/controllers/full-page-search.js +++ b/app/assets/javascripts/discourse/app/controllers/full-page-search.js @@ -77,9 +77,9 @@ export default Controller.extend({ @discourseComputed("context", "context_id") searchContextDescription(context, id) { - var name = id; + let name = id; if (context === "category") { - var category = Category.findById(id); + let category = Category.findById(id); if (!category) { return; } @@ -322,7 +322,7 @@ export default Controller.extend({ }, loadMore() { - var page = this.page; + let page = this.page; if ( this.get("model.grouped_search_result.more_full_page_results") && !this.loading && diff --git a/app/assets/javascripts/discourse/app/controllers/group.js b/app/assets/javascripts/discourse/app/controllers/group.js index 635a1bca1e..3d10fc9f02 100644 --- a/app/assets/javascripts/discourse/app/controllers/group.js +++ b/app/assets/javascripts/discourse/app/controllers/group.js @@ -142,13 +142,22 @@ export default Controller.extend({ destroyGroup() { this.set("destroying", true); + const model = this.model; + let message = I18n.t("admin.groups.delete_confirm"); + + if (model.has_messages && model.message_count > 0) { + message = I18n.t("admin.groups.delete_with_messages_confirm", { + count: model.message_count, + }); + } + bootbox.confirm( - I18n.t("admin.groups.delete_confirm"), + message, I18n.t("no_value"), I18n.t("yes_value"), (confirmed) => { if (confirmed) { - this.model + model .destroy() .then(() => this.transitionToRoute("groups.index")) .catch((error) => { diff --git a/app/assets/javascripts/discourse/app/controllers/history.js b/app/assets/javascripts/discourse/app/controllers/history.js index 23d66efe33..b6fd92cc7e 100644 --- a/app/assets/javascripts/discourse/app/controllers/history.js +++ b/app/assets/javascripts/discourse/app/controllers/history.js @@ -17,7 +17,7 @@ import { sanitizeAsync } from "discourse/lib/text"; function customTagArray(fieldName) { return computed(fieldName, function () { - var val = this.get(fieldName); + let val = this.get(fieldName); if (!val) { return val; } @@ -194,7 +194,7 @@ export default Controller.extend(ModalFunctionality, { if (displayingInline) { return this.isEitherRevisionHidden ? "hidden-revision-either" : null; } else { - var result = []; + let result = []; if (prevHidden) { result.push("hidden-revision-previous"); } @@ -227,7 +227,7 @@ export default Controller.extend(ModalFunctionality, { @discourseComputed("model.category_id_changes") previousCategory(changes) { if (changes) { - var category = Category.findById(changes["previous"]); + let category = Category.findById(changes["previous"]); return categoryBadgeHTML(category, { allowUncategorized: true }); } }, @@ -235,7 +235,7 @@ export default Controller.extend(ModalFunctionality, { @discourseComputed("model.category_id_changes") currentCategory(changes) { if (changes) { - var category = Category.findById(changes["current"]); + let category = Category.findById(changes["current"]); return categoryBadgeHTML(category, { allowUncategorized: true }); } }, diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index ce500f8209..481b70453f 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -403,6 +403,8 @@ export default Controller.extend(ModalFunctionality, { skipConfirmation, }); - showModal("createAccount", { modalClass: "create-account" }); + next(() => { + showModal("createAccount", { modalClass: "create-account" }); + }); }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/emails.js b/app/assets/javascripts/discourse/app/controllers/preferences/emails.js index 2062170002..2709ebe99e 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/emails.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/emails.js @@ -62,7 +62,7 @@ export default Controller.extend({ @discourseComputed() frequencyEstimate() { - var estimate = this.get("model.mailing_list_posts_per_day"); + let estimate = this.get("model.mailing_list_posts_per_day"); if (!estimate || estimate < 2) { return I18n.t("user.mailing_list_mode.few_per_day"); } else { diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js index aff0e805b8..2ae8253e64 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js @@ -153,7 +153,7 @@ export default Controller.extend({ "themeId" ) currentSchemeCanBeSelected(userThemes, userColorSchemes, themeId) { - if (!userThemes) { + if (!userThemes || !themeId) { return false; } diff --git a/app/assets/javascripts/discourse/app/controllers/reorder-categories.js b/app/assets/javascripts/discourse/app/controllers/reorder-categories.js index 0875848ce3..c0b7778018 100644 --- a/app/assets/javascripts/discourse/app/controllers/reorder-categories.js +++ b/app/assets/javascripts/discourse/app/controllers/reorder-categories.js @@ -15,7 +15,7 @@ export default Controller.extend(ModalFunctionality, Evented, { this.categoriesSorting = ["position"]; }, - @discourseComputed("site.categories") + @discourseComputed("site.categories.[]") categoriesBuffered(categories) { const bufProxy = EmberObjectProxy.extend(BufferedProxy); return categories.map((c) => bufProxy.create({ content: c })); diff --git a/app/assets/javascripts/discourse/app/controllers/tags-index.js b/app/assets/javascripts/discourse/app/controllers/tags-index.js index 7222df90c9..60acd67e74 100644 --- a/app/assets/javascripts/discourse/app/controllers/tags-index.js +++ b/app/assets/javascripts/discourse/app/controllers/tags-index.js @@ -72,7 +72,7 @@ export default Controller.extend({ } const joinedTags = tags.slice(0, displayN).join(", "); - var more = Math.max(0, tags.length - displayN); + let more = Math.max(0, tags.length - displayN); const tagsString = more === 0 diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index e050aca6ea..7240ee39e9 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -702,9 +702,9 @@ export default Controller.extend(bufferedProperty("model"), { if (!this.currentUser) { return bootbox.alert(I18n.t("bookmarks.not_bookmarked")); } else if (post) { - return post.toggleBookmark(); + return this._togglePostBookmark(post); } else { - return this.model.toggleBookmark().then((changedIds) => { + return this._toggleTopicBookmark(this.model).then((changedIds) => { if (!changedIds) { return; } @@ -1167,6 +1167,107 @@ export default Controller.extend(bufferedProperty("model"), { } }, + _togglePostBookmark(post) { + return new Promise((resolve) => { + let modalController = showModal("bookmark", { + model: { + postId: post.id, + id: post.bookmark_id, + reminderAt: post.bookmark_reminder_at, + autoDeletePreference: post.bookmark_auto_delete_preference, + name: post.bookmark_name, + }, + title: post.bookmark_id + ? "post.bookmarks.edit" + : "post.bookmarks.create", + modalClass: "bookmark-with-reminder", + }); + modalController.setProperties({ + onCloseWithoutSaving: () => { + resolve({ closedWithoutSaving: true }); + post.appEvents.trigger("post-stream:refresh", { id: post.id }); + }, + afterSave: (savedData) => { + post.createBookmark(savedData); + resolve({ closedWithoutSaving: false }); + }, + afterDelete: (topicBookmarked) => { + post.deleteBookmark(topicBookmarked); + }, + }); + }); + }, + + _toggleTopicBookmark() { + if (this.model.bookmarking) { + return Promise.resolve(); + } + this.model.set("bookmarking", true); + const bookmark = !this.model.bookmarked; + let posts = this.model.postStream.posts; + + return this.model.firstPost().then((firstPost) => { + const toggleBookmarkOnServer = () => { + if (bookmark) { + return this._togglePostBookmark(firstPost).then((opts) => { + this.model.set("bookmarking", false); + if (opts && opts.closedWithoutSaving) { + return; + } + return this.model.afterTopicBookmarked(firstPost); + }); + } else { + return this.model + .deleteBookmark() + .then(() => { + this.model.toggleProperty("bookmarked"); + this.model.set("bookmark_reminder_at", null); + let clearedBookmarkProps = { + bookmarked: false, + bookmark_id: null, + bookmark_name: null, + bookmark_reminder_at: null, + }; + if (posts) { + const updated = []; + posts.forEach((post) => { + if (post.bookmarked) { + post.setProperties(clearedBookmarkProps); + updated.push(post.id); + } + }); + firstPost.setProperties(clearedBookmarkProps); + return updated; + } + }) + .catch(popupAjaxError) + .finally(() => this.model.set("bookmarking", false)); + } + }; + + const unbookmarkedPosts = []; + if (!bookmark && posts) { + posts.forEach( + (post) => post.bookmarked && unbookmarkedPosts.push(post) + ); + } + + return new Promise((resolve) => { + if (unbookmarkedPosts.length > 1) { + bootbox.confirm( + I18n.t("bookmarks.confirm_clear"), + I18n.t("no_value"), + I18n.t("yes_value"), + (confirmed) => + confirmed ? toggleBookmarkOnServer().then(resolve) : resolve() + ); + } else { + toggleBookmarkOnServer().then(resolve); + } + }); + }); + }, + togglePinnedState() { this.send("togglePinnedForUser"); }, diff --git a/app/assets/javascripts/discourse/app/controllers/user-activity.js b/app/assets/javascripts/discourse/app/controllers/user-activity.js index d6e7241f35..f2023c232b 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-activity.js +++ b/app/assets/javascripts/discourse/app/controllers/user-activity.js @@ -16,7 +16,7 @@ export default Controller.extend({ @observes("userActionType", "model.stream.itemsLoaded") _showFooter: function () { - var showFooter; + let showFooter; if (this.userActionType) { const stat = (this.get("model.stats") || []).find( (s) => s.action_type === this.userActionType diff --git a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js index 4e1fd70393..e14955789e 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js +++ b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js @@ -40,7 +40,7 @@ export default Controller.extend({ bulkOperation(operation) { const selected = this.selected; - var params = { type: operation }; + let params = { type: operation }; if (this.isGroup) { params.group = this.groupFilter; } diff --git a/app/assets/javascripts/discourse/app/controllers/user.js b/app/assets/javascripts/discourse/app/controllers/user.js index fa9e99749b..a2b531f8a3 100644 --- a/app/assets/javascripts/discourse/app/controllers/user.js +++ b/app/assets/javascripts/discourse/app/controllers/user.js @@ -3,7 +3,11 @@ import EmberObject, { computed, set } from "@ember/object"; import { alias, and, gt, not, or } from "@ember/object/computed"; import CanCheckEmails from "discourse/mixins/can-check-emails"; import User from "discourse/models/user"; +import I18n from "I18n"; +import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; +import getURL from "discourse-common/lib/get-url"; +import { iconHTML } from "discourse-common/lib/icon-library"; import { isEmpty } from "@ember/utils"; import optionalService from "discourse/lib/optional-service"; import { prioritizeNameInUx } from "discourse/lib/settings"; @@ -168,7 +172,57 @@ export default Controller.extend(CanCheckEmails, { }, adminDelete() { - this.adminTools.deleteUser(this.get("model.id")); + const userId = this.get("model.id"); + const message = I18n.t("admin.user.delete_confirm"); + const location = document.location.pathname; + + const performDestroy = (block) => { + bootbox.dialog(I18n.t("admin.user.deleting_user")); + let formData = { context: location }; + if (block) { + formData["block_email"] = true; + formData["block_urls"] = true; + formData["block_ip"] = true; + } + formData["delete_posts"] = true; + + this.adminTools + .deleteUser(userId, formData) + .then((data) => { + if (data.deleted) { + document.location = getURL("/admin/users/list/active"); + } else { + bootbox.alert(I18n.t("admin.user.delete_failed")); + } + }) + .catch(() => bootbox.alert(I18n.t("admin.user.delete_failed"))); + }; + + const buttons = [ + { + label: I18n.t("composer.cancel"), + class: "btn", + link: true, + }, + { + label: + `${iconHTML("exclamation-triangle")} ` + + I18n.t("admin.user.delete_and_block"), + class: "btn btn-danger", + callback: function () { + performDestroy(true); + }, + }, + { + label: I18n.t("admin.user.delete_dont_block"), + class: "btn btn-primary", + callback: function () { + performDestroy(false); + }, + }, + ]; + + bootbox.dialog(message, buttons, { classes: "delete-user-modal" }); }, updateNotificationLevel(level) { diff --git a/app/assets/javascripts/discourse/app/helpers/category-link.js b/app/assets/javascripts/discourse/app/helpers/category-link.js index 71dcd3d722..289b0407d9 100644 --- a/app/assets/javascripts/discourse/app/helpers/category-link.js +++ b/app/assets/javascripts/discourse/app/helpers/category-link.js @@ -16,7 +16,7 @@ export function replaceCategoryLinkRenderer(fn) { } function categoryStripe(color, classes) { - var style = color ? "style='background-color: #" + color + ";'" : ""; + let style = color ? "style='background-color: #" + color + ";'" : ""; return ""; } @@ -65,7 +65,7 @@ export function categoryBadgeHTML(category, opts) { } export function categoryLinkHTML(category, options) { - var categoryOptions = {}; + let categoryOptions = {}; // TODO: This is a compatibility layer with the old helper structure. // Can be removed once we migrate to `registerUnbound` fully diff --git a/app/assets/javascripts/discourse/app/helpers/format-date.js b/app/assets/javascripts/discourse/app/helpers/format-date.js index e91759cdce..1798b23e02 100644 --- a/app/assets/javascripts/discourse/app/helpers/format-date.js +++ b/app/assets/javascripts/discourse/app/helpers/format-date.js @@ -7,7 +7,7 @@ import { registerUnbound } from "discourse-common/lib/helpers"; update the dates on a regular interval. **/ registerUnbound("format-date", function (val, params) { - var leaveAgo, + let leaveAgo, format = "medium", title = true; @@ -22,7 +22,7 @@ registerUnbound("format-date", function (val, params) { } if (val) { - var date = new Date(val); + let date = new Date(val); return htmlSafe( autoUpdatingRelativeAge(date, { format: format, diff --git a/app/assets/javascripts/discourse/app/helpers/loading-spinner.js b/app/assets/javascripts/discourse/app/helpers/loading-spinner.js index 5f15403d05..5188c61990 100644 --- a/app/assets/javascripts/discourse/app/helpers/loading-spinner.js +++ b/app/assets/javascripts/discourse/app/helpers/loading-spinner.js @@ -1,13 +1,13 @@ import { htmlHelper } from "discourse-common/lib/helpers"; function renderSpinner(cssClass) { - var html = "
"; } -var spinnerHTML = renderSpinner(); +let spinnerHTML = renderSpinner(); export default htmlHelper((params) => { const hash = params.hash; diff --git a/app/assets/javascripts/discourse/app/helpers/shorten-url.js b/app/assets/javascripts/discourse/app/helpers/shorten-url.js index 5b7012a339..4a35906190 100644 --- a/app/assets/javascripts/discourse/app/helpers/shorten-url.js +++ b/app/assets/javascripts/discourse/app/helpers/shorten-url.js @@ -1,7 +1,7 @@ import { registerUnbound } from "discourse-common/lib/helpers"; registerUnbound("shorten-url", function (url) { - var matches = url.match(/\//g); + let matches = url.match(/\//g); if (matches && matches.length === 3) { url = url.replace(/\/$/, ""); diff --git a/app/assets/javascripts/discourse/app/initializers/inject-objects.js b/app/assets/javascripts/discourse/app/initializers/inject-objects.js index abcf14a574..533e9134af 100644 --- a/app/assets/javascripts/discourse/app/initializers/inject-objects.js +++ b/app/assets/javascripts/discourse/app/initializers/inject-objects.js @@ -1,6 +1,9 @@ -// backwards compatibility for plugins that depend on this initializer +import { setDefaultOwner } from "discourse-common/lib/get-owner"; export default { name: "inject-objects", - initialize() {}, + initialize(container, app) { + // This is required for Ember CLI tests to work + setDefaultOwner(app.__container__); + }, }; diff --git a/app/assets/javascripts/discourse/app/lib/after-transition.js b/app/assets/javascripts/discourse/app/lib/after-transition.js index 16c57b53cf..70edaaf843 100644 --- a/app/assets/javascripts/discourse/app/lib/after-transition.js +++ b/app/assets/javascripts/discourse/app/lib/after-transition.js @@ -4,7 +4,7 @@ SO: http://stackoverflow.com/questions/9943435/css3-animation-end-techniques **/ -var dummy = document.createElement("div"), +let dummy = document.createElement("div"), eventNameHash = { webkit: "webkitTransitionEnd", Moz: "transitionend", @@ -12,8 +12,8 @@ var dummy = document.createElement("div"), ms: "MSTransitionEnd", }; -var transitionEnd = (function () { - var retValue; +let transitionEnd = (function () { + let retValue; retValue = "transitionend"; Object.keys(eventNameHash).some(function (vendor) { if (vendor + "TransitionProperty" in dummy.style) { diff --git a/app/assets/javascripts/discourse/app/lib/category-hashtags.js b/app/assets/javascripts/discourse/app/lib/category-hashtags.js index 75c85616f9..18cf98b899 100644 --- a/app/assets/javascripts/discourse/app/lib/category-hashtags.js +++ b/app/assets/javascripts/discourse/app/lib/category-hashtags.js @@ -15,8 +15,8 @@ export function replaceSpan($elem, categorySlug, categoryLink, type) { export function categoryHashtagTriggerRule(textarea, opts) { const result = caretRowCol(textarea); const row = result.rowNum; - var col = result.colNum; - var line = textarea.value.split("\n")[row - 1]; + let col = result.colNum; + let line = textarea.value.split("\n")[row - 1]; if (opts && opts.backSpace) { col = col - 1; diff --git a/app/assets/javascripts/discourse/app/lib/category-tag-search.js b/app/assets/javascripts/discourse/app/lib/category-tag-search.js index d2e1fd5c83..ef84ac811f 100644 --- a/app/assets/javascripts/discourse/app/lib/category-tag-search.js +++ b/app/assets/javascripts/discourse/app/lib/category-tag-search.js @@ -37,7 +37,7 @@ function searchTags(term, categories, limit) { data: { limit: limit, q }, }); - var returnVal = CANCELLED_STATUS; + let returnVal = CANCELLED_STATUS; oldSearch .then((r) => { @@ -91,8 +91,8 @@ export function search(term, siteSettings) { } const limit = 5; - var categories = Category.search(term, { limit }); - var numOfCategories = categories.length; + let categories = Category.search(term, { limit }); + let numOfCategories = categories.length; categories = categories.map((category) => { return { model: category, text: Category.slugFor(category, SEPARATOR, 2) }; diff --git a/app/assets/javascripts/discourse/app/lib/mobile.js b/app/assets/javascripts/discourse/app/lib/mobile.js index e7c1543948..a35cfcee6d 100644 --- a/app/assets/javascripts/discourse/app/lib/mobile.js +++ b/app/assets/javascripts/discourse/app/lib/mobile.js @@ -27,7 +27,7 @@ const Mobile = { localStorage.removeItem("mobileView"); } if (localStorage.mobileView) { - var savedValue = localStorage.mobileView === "true"; + let savedValue = localStorage.mobileView === "true"; if (savedValue !== this.mobileView) { this.reloadPage(savedValue); } diff --git a/app/assets/javascripts/discourse/app/lib/page-visible.js b/app/assets/javascripts/discourse/app/lib/page-visible.js index 7733a9d747..0771a2f3c0 100644 --- a/app/assets/javascripts/discourse/app/lib/page-visible.js +++ b/app/assets/javascripts/discourse/app/lib/page-visible.js @@ -1,5 +1,5 @@ // for android we test webkit -var hiddenProperty = +let hiddenProperty = document.hidden !== undefined ? "hidden" : document.webkitHidden !== undefined diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index b536e80dbb..636c8207a7 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -737,10 +737,10 @@ class PluginApi { * example: * * api.addUserMenuGlyph({ - * label: 'awesome.label', + * title: 'awesome.label', * className: 'my-class', * icon: 'my-icon', - * href: `/some/path` + * data: { url: `/some/path` }, * }); * */ @@ -1227,11 +1227,11 @@ let _pluginv01; // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number function cmpVersions(a, b) { - var i, diff; - var regExStrip0 = /(\.0+)+$/; - var segmentsA = a.replace(regExStrip0, "").split("."); - var segmentsB = b.replace(regExStrip0, "").split("."); - var l = Math.min(segmentsA.length, segmentsB.length); + let i, diff; + let regExStrip0 = /(\.0+)+$/; + let segmentsA = a.replace(regExStrip0, "").split("."); + let segmentsB = b.replace(regExStrip0, "").split("."); + let l = Math.min(segmentsA.length, segmentsB.length); for (i = 0; i < l; i++) { diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10); diff --git a/app/assets/javascripts/discourse/app/lib/safari-hacks.js b/app/assets/javascripts/discourse/app/lib/safari-hacks.js index 99c8b335e1..1bea1a5f07 100644 --- a/app/assets/javascripts/discourse/app/lib/safari-hacks.js +++ b/app/assets/javascripts/discourse/app/lib/safari-hacks.js @@ -93,7 +93,7 @@ function positioningWorkaround($fixedElement) { const fixedElement = $fixedElement[0]; const oldHeight = fixedElement.style.height; - var originalScrollTop = 0; + let originalScrollTop = 0; let lastTouchedElement = null; positioningWorkaround.blur = function (evt) { @@ -114,7 +114,7 @@ function positioningWorkaround($fixedElement) { } }; - var blurredNow = function (evt) { + let blurredNow = function (evt) { // we cannot use evt.relatedTarget to get the last focused element in safari iOS // document.activeElement is also unreliable (iOS does not mark buttons as focused) // so instead, we store the last touched element and check against it @@ -145,11 +145,11 @@ function positioningWorkaround($fixedElement) { positioningWorkaround.blur(evt); }; - var blurred = function (evt) { + let blurred = function (evt) { discourseDebounce(this, blurredNow, evt, INPUT_DELAY); }; - var positioningHack = function (evt) { + let positioningHack = function (evt) { let _this = this; if (evt === undefined) { @@ -203,7 +203,7 @@ function positioningWorkaround($fixedElement) { }, delay); }; - var lastTouched = function (evt) { + let lastTouched = function (evt) { if (evt && evt.target) { lastTouchedElement = evt.target; } @@ -231,7 +231,7 @@ function positioningWorkaround($fixedElement) { }; positioningWorkaround.touchstartEvent = function (element) { - var triggerHack = positioningHack.bind(element); + let triggerHack = positioningHack.bind(element); triggerHack(); }; diff --git a/app/assets/javascripts/discourse/app/lib/text-direction.js b/app/assets/javascripts/discourse/app/lib/text-direction.js index 013cbdaed4..b3da0964b6 100644 --- a/app/assets/javascripts/discourse/app/lib/text-direction.js +++ b/app/assets/javascripts/discourse/app/lib/text-direction.js @@ -29,3 +29,7 @@ export function siteDir() { } return _siteDir; } + +export function isDocumentRTL() { + return siteDir() === "rtl"; +} diff --git a/app/assets/javascripts/discourse/app/lib/user-search.js b/app/assets/javascripts/discourse/app/lib/user-search.js index c77c5c6cd7..980d92a417 100644 --- a/app/assets/javascripts/discourse/app/lib/user-search.js +++ b/app/assets/javascripts/discourse/app/lib/user-search.js @@ -6,7 +6,7 @@ import { emailValid } from "discourse/lib/utilities"; import { isTesting } from "discourse-common/config/environment"; import { userPath } from "discourse/lib/url"; -var cache = {}, +let cache = {}, cacheKey, cacheTime, currentTerm, @@ -23,7 +23,7 @@ function performSearch( groupMembersOf, resultsFn ) { - var cached = cache[term]; + let cached = cache[term]; if (cached) { resultsFn(cached); return; @@ -52,7 +52,7 @@ function performSearch( }, }); - var returnVal = CANCELLED_STATUS; + let returnVal = CANCELLED_STATUS; oldSearch .then(function (r) { @@ -81,7 +81,7 @@ function performSearch( }); } -var debouncedSearch = function ( +let debouncedSearch = function ( term, topicId, categoryId, @@ -113,7 +113,7 @@ function organizeResults(r, options) { return r; } - var exclude = options.exclude || [], + let exclude = options.exclude || [], limit = options.limit || 5, users = [], emails = [], @@ -182,7 +182,7 @@ export default function userSearch(options) { options.term = options.term.substring(1); } - var term = options.term || "", + let term = options.term || "", includeGroups = options.includeGroups, includeMentionableGroups = options.includeMentionableGroups, includeMessageableGroups = options.includeMessageableGroups, diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index 6267aa8f21..f7b7bad13e 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -97,7 +97,7 @@ export function tinyAvatar(avatarTemplate, options) { } export function postUrl(slug, topicId, postNumber) { - var url = getURL("/t/"); + let url = getURL("/t/"); if (slug) { url += slug + "/"; } else { @@ -190,11 +190,11 @@ export function selectedElement() { // Determine the row and col of the caret in an element export function caretRowCol(el) { - var cp = caretPosition(el); - var rows = el.value.slice(0, cp).split("\n"); - var rowNum = rows.length; + let cp = caretPosition(el); + let rows = el.value.slice(0, cp).split("\n"); + let rowNum = rows.length; - var colNum = + let colNum = cp - rows.splice(0, rowNum - 1).reduce(function (sum, row) { return sum + row.length + 1; @@ -205,7 +205,7 @@ export function caretRowCol(el) { // Determine the position of the caret in an element export function caretPosition(el) { - var r, rc, re; + let r, rc, re; if (el.selectionStart) { return el.selectionStart; } @@ -227,7 +227,7 @@ export function caretPosition(el) { // Set the caret's position export function setCaretPosition(ctrl, pos) { - var range; + let range; if (ctrl.setSelectionRange) { ctrl.focus(); ctrl.setSelectionRange(pos, pos); @@ -421,7 +421,7 @@ export function areCookiesEnabled() { // see: https://github.com/Modernizr/Modernizr/blob/400db4043c22af98d46e1d2b9cbc5cb062791192/feature-detects/cookies.js try { document.cookie = "cookietest=1"; - var ret = document.cookie.indexOf("cookietest=") !== -1; + let ret = document.cookie.indexOf("cookietest=") !== -1; document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT"; return ret; } catch (e) { diff --git a/app/assets/javascripts/discourse/app/mapping-router.js b/app/assets/javascripts/discourse/app/mapping-router.js index 3f332c6e42..39ab250c8f 100644 --- a/app/assets/javascripts/discourse/app/mapping-router.js +++ b/app/assets/javascripts/discourse/app/mapping-router.js @@ -113,7 +113,7 @@ export function mapRoutes() { // can define admin routes. Object.keys(requirejs._eak_seen).forEach(function (key) { if (/route-map$/.test(key)) { - var module = requirejs(key, null, null, true); + let module = requirejs(key, null, null, true); if (!module || !module.default) { throw new Error(key + " must export a route map."); } diff --git a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js index d539a14063..2908384952 100644 --- a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js +++ b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js @@ -3,8 +3,11 @@ import { NotificationLevels } from "discourse/lib/notification-levels"; import Topic from "discourse/models/topic"; import { alias } from "@ember/object/computed"; import { on } from "discourse-common/utils/decorators"; +import { inject as service } from "@ember/service"; export default Mixin.create({ + router: service(), + bulkSelectEnabled: false, selected: null, diff --git a/app/assets/javascripts/discourse/app/mixins/singleton.js b/app/assets/javascripts/discourse/app/mixins/singleton.js index 6bed17037e..7aabd44c35 100644 --- a/app/assets/javascripts/discourse/app/mixins/singleton.js +++ b/app/assets/javascripts/discourse/app/mixins/singleton.js @@ -67,7 +67,7 @@ const Singleton = Mixin.create({ // Returns OR sets a property on the singleton instance. currentProp(property, value) { - var instance = this.current(); + let instance = this.current(); if (!instance) { return; } diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index 2340a7213d..b6b84f4634 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -312,7 +312,7 @@ const Category = RestModel.extend({ }, }); -var _uncategorized; +let _uncategorized; Category.reopenClass({ slugEncoded() { @@ -508,7 +508,7 @@ Category.reopenClass({ }, search(term, opts) { - var limit = 5; + let limit = 5; if (opts) { if (opts.limit === 0) { @@ -529,8 +529,8 @@ Category.reopenClass({ const categories = Category.listByActivity(); const length = categories.length; - var i; - var data = []; + let i; + let data = []; const done = () => { return data.length === limit; diff --git a/app/assets/javascripts/discourse/app/models/nav-item.js b/app/assets/javascripts/discourse/app/models/nav-item.js index 6c1fe8213c..d369d05f36 100644 --- a/app/assets/javascripts/discourse/app/models/nav-item.js +++ b/app/assets/javascripts/discourse/app/models/nav-item.js @@ -177,7 +177,7 @@ NavItem.reopenClass({ return null; } - var args = { name: filterType, hasIcon: filterType === "unread" }; + let args = { name: filterType, hasIcon: filterType === "unread" }; if (opts.category) { args.category = opts.category; } diff --git a/app/assets/javascripts/discourse/app/models/post.js b/app/assets/javascripts/discourse/app/models/post.js index 5a8acd7c96..4c3a671ddf 100644 --- a/app/assets/javascripts/discourse/app/models/post.js +++ b/app/assets/javascripts/discourse/app/models/post.js @@ -16,7 +16,6 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; import { postUrl } from "discourse/lib/utilities"; import { propertyEqual } from "discourse/lib/computed"; import { resolveShareUrl } from "discourse/helpers/share-url"; -import showModal from "discourse/lib/show-modal"; import { userPath } from "discourse/lib/url"; const Post = RestModel.extend({ @@ -304,58 +303,24 @@ const Post = RestModel.extend({ return ajax(`/posts/${this.id}/unhide`, { type: "PUT" }); }, - toggleBookmark() { - let postEl = document.querySelector(`[data-post-id="${this.id}"]`); - let localDateEl = null; - if (postEl) { - localDateEl = postEl.querySelector(".discourse-local-date"); - } - - return new Promise((resolve) => { - let controller = showModal("bookmark", { - model: { - postId: this.id, - id: this.bookmark_id, - reminderAt: this.bookmark_reminder_at, - autoDeletePreference: this.bookmark_auto_delete_preference, - name: this.bookmark_name, - postDetectedLocalDate: localDateEl ? localDateEl.dataset.date : null, - postDetectedLocalTime: localDateEl ? localDateEl.dataset.time : null, - postDetectedLocalTimezone: localDateEl - ? localDateEl.dataset.timezone - : null, - }, - title: this.bookmark_id - ? "post.bookmarks.edit" - : "post.bookmarks.create", - modalClass: "bookmark-with-reminder", - }); - controller.setProperties({ - onCloseWithoutSaving: () => { - resolve({ closedWithoutSaving: true }); - this.appEvents.trigger("post-stream:refresh", { id: this.id }); - }, - afterSave: (savedData) => { - this.setProperties({ - "topic.bookmarked": true, - bookmarked: true, - bookmark_reminder_at: savedData.reminderAt, - bookmark_reminder_type: savedData.reminderType, - bookmark_auto_delete_preference: savedData.autoDeletePreference, - bookmark_name: savedData.name, - bookmark_id: savedData.id, - }); - resolve({ closedWithoutSaving: false }); - this.appEvents.trigger("page:bookmark-post-toggled", this); - this.appEvents.trigger("post-stream:refresh", { id: this.id }); - }, - afterDelete: (topicBookmarked) => { - this.set("topic.bookmarked", topicBookmarked); - this.clearBookmark(); - this.appEvents.trigger("page:bookmark-post-toggled", this); - }, - }); + createBookmark(data) { + this.setProperties({ + "topic.bookmarked": true, + bookmarked: true, + bookmark_reminder_at: data.reminderAt, + bookmark_reminder_type: data.reminderType, + bookmark_auto_delete_preference: data.autoDeletePreference, + bookmark_name: data.name, + bookmark_id: data.id, }); + this.appEvents.trigger("page:bookmark-post-toggled", this); + this.appEvents.trigger("post-stream:refresh", { id: this.id }); + }, + + deleteBookmark(bookmarked) { + this.set("topic.bookmarked", bookmarked); + this.clearBookmark(); + this.appEvents.trigger("page:bookmark-post-toggled", this); }, clearBookmark() { diff --git a/app/assets/javascripts/discourse/app/models/store.js b/app/assets/javascripts/discourse/app/models/store.js index aa477b0ee1..d1783c08ef 100644 --- a/app/assets/javascripts/discourse/app/models/store.js +++ b/app/assets/javascripts/discourse/app/models/store.js @@ -114,9 +114,9 @@ export default EmberObject.extend({ }, find(type, findArgs, opts) { - var adapter = this.adapterFor(type); + let adapter = this.adapterFor(type); return adapter.find(this, type, findArgs, opts).then((result) => { - var hydrated = this._hydrateFindResults(result, type, findArgs, opts); + let hydrated = this._hydrateFindResults(result, type, findArgs, opts); if (result.extras) { hydrated.set("extras", result.extras); @@ -139,7 +139,7 @@ export default EmberObject.extend({ hydrated.set( "content", hydrated.get("content").map((item) => { - var staleItem = stale.content.findBy(primaryKey, item.get(primaryKey)); + let staleItem = stale.content.findBy(primaryKey, item.get(primaryKey)); if (staleItem) { staleItem.setProperties(item); } else { diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index d2ceed6a3a..71a2bb709b 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -291,7 +291,7 @@ const TopicTrackingState = EmberObject.extend({ if (split.length >= 4) { filter = split[split.length - 1]; // c/cat/subcat/6/l/latest - var category = Category.findSingleBySlug( + let category = Category.findSingleBySlug( split.splice(1, split.length - 4).join("/") ); this.set("filterCategory", category); diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index 8796bc4261..6de4b88f73 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -11,7 +11,6 @@ import Session from "discourse/models/session"; import Site from "discourse/models/site"; import User from "discourse/models/user"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import { deepMerge } from "discourse-common/lib/object"; import discourseComputed from "discourse-common/utils/decorators"; import { emojiUnescape } from "discourse/lib/text"; @@ -404,73 +403,8 @@ const Topic = RestModel.extend({ } }, - toggleBookmark() { - if (this.bookmarking) { - return Promise.resolve(); - } - this.set("bookmarking", true); - const bookmark = !this.bookmarked; - let posts = this.postStream.posts; - - return this.firstPost().then((firstPost) => { - const toggleBookmarkOnServer = () => { - if (bookmark) { - return firstPost.toggleBookmark().then((opts) => { - this.set("bookmarking", false); - if (opts.closedWithoutSaving) { - return; - } - return this.afterTopicBookmarked(firstPost); - }); - } else { - return ajax(`/t/${this.id}/remove_bookmarks`, { type: "PUT" }) - .then(() => { - this.toggleProperty("bookmarked"); - this.set("bookmark_reminder_at", null); - let clearedBookmarkProps = { - bookmarked: false, - bookmark_id: null, - bookmark_name: null, - bookmark_reminder_at: null, - }; - if (posts) { - const updated = []; - posts.forEach((post) => { - if (post.bookmarked) { - post.setProperties(clearedBookmarkProps); - updated.push(post.id); - } - }); - firstPost.setProperties(clearedBookmarkProps); - return updated; - } - }) - .catch(popupAjaxError) - .finally(() => this.set("bookmarking", false)); - } - }; - - const unbookmarkedPosts = []; - if (!bookmark && posts) { - posts.forEach( - (post) => post.bookmarked && unbookmarkedPosts.push(post) - ); - } - - return new Promise((resolve) => { - if (unbookmarkedPosts.length > 1) { - bootbox.confirm( - I18n.t("bookmarks.confirm_clear"), - I18n.t("no_value"), - I18n.t("yes_value"), - (confirmed) => - confirmed ? toggleBookmarkOnServer().then(resolve) : resolve() - ); - } else { - toggleBookmarkOnServer().then(resolve); - } - }); - }); + deleteBookmark() { + return ajax(`/t/${this.id}/remove_bookmarks`, { type: "PUT" }); }, createGroupInvite(group) { diff --git a/app/assets/javascripts/discourse/app/models/user-badge.js b/app/assets/javascripts/discourse/app/models/user-badge.js index 304059d65d..0aaff3f561 100644 --- a/app/assets/javascripts/discourse/app/models/user-badge.js +++ b/app/assets/javascripts/discourse/app/models/user-badge.js @@ -27,7 +27,7 @@ UserBadge.reopenClass({ if (json.users === undefined) { json.users = []; } - var users = {}; + let users = {}; json.users.forEach(function (userJson) { users[userJson.id] = User.create(userJson); }); @@ -36,7 +36,7 @@ UserBadge.reopenClass({ if (json.topics === undefined) { json.topics = []; } - var topics = {}; + let topics = {}; json.topics.forEach(function (topicJson) { topics[topicJson.id] = Topic.create(topicJson); }); @@ -45,13 +45,13 @@ UserBadge.reopenClass({ if (json.badges === undefined) { json.badges = []; } - var badges = {}; + let badges = {}; Badge.createFromJson(json).forEach(function (badge) { badges[badge.get("id")] = badge; }); // Create UserBadge object(s). - var userBadges = []; + let userBadges = []; if ("user_badge" in json) { userBadges = [json.user_badge]; } else { @@ -61,9 +61,9 @@ UserBadge.reopenClass({ } userBadges = userBadges.map(function (userBadgeJson) { - var userBadge = UserBadge.create(userBadgeJson); + let userBadge = UserBadge.create(userBadgeJson); - var grantedAtDate = Date.parse(userBadge.get("granted_at")); + let grantedAtDate = Date.parse(userBadge.get("granted_at")); userBadge.set("grantedAt", grantedAtDate); userBadge.set("badge", badges[userBadge.get("badge_id")]); @@ -102,7 +102,7 @@ UserBadge.reopenClass({ if (!username) { return Promise.resolve([]); } - var url = "/user-badges/" + username + ".json"; + let url = "/user-badges/" + username + ".json"; if (options && options.grouped) { url += "?grouped=true"; } diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 7a0b562b43..61f34556f6 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -342,7 +342,7 @@ const User = RestModel.extend({ data[s] = this.get(`user_option.${s}`); }); - var updatedState = {}; + let updatedState = {}; ["muted", "regular", "watched", "tracked", "watched_first_post"].forEach( (s) => { diff --git a/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js b/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js index 8c4b69dd0f..d064dd3b32 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js @@ -1,5 +1,5 @@ import Application from "@ember/application"; -import Ember from "ember"; +import { isLegacyEmber } from "discourse-common/config/environment"; import { registerRouter } from "discourse/mapping-router"; let originalBuildInstance; @@ -12,8 +12,7 @@ export default { let router = registerRouter(app); container.registry.register("router:main", router); - // TODO: Remove this once we've upgraded Ember everywhere - if (Ember.VERSION.startsWith("3.12")) { + if (isLegacyEmber()) { // HACK to fix: https://github.com/emberjs/ember.js/issues/10310 originalBuildInstance = originalBuildInstance || Application.prototype.buildInstance; diff --git a/app/assets/javascripts/discourse/app/pre-initializers/register-dom-templates.js b/app/assets/javascripts/discourse/app/pre-initializers/register-dom-templates.js index 27b511eb53..d3f6dbe3c6 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/register-dom-templates.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/register-dom-templates.js @@ -3,8 +3,8 @@ export default { initialize: function () { $('script[type="text/x-handlebars"]').each(function () { - var $this = $(this); - var name = $this.attr("name") || $this.data("template-name"); + let $this = $(this); + let name = $this.attr("name") || $this.data("template-name"); if (window.console) { window.console.log( diff --git a/app/assets/javascripts/discourse/app/routes/build-category-route.js b/app/assets/javascripts/discourse/app/routes/build-category-route.js index 8672db77c1..5b77d14f82 100644 --- a/app/assets/javascripts/discourse/app/routes/build-category-route.js +++ b/app/assets/javascripts/discourse/app/routes/build-category-route.js @@ -157,7 +157,7 @@ export default (filterArg, params) => { canCreateTopic: canCreateTopic, }); - var topicOpts = { + let topicOpts = { model: topics, category, period: diff --git a/app/assets/javascripts/discourse/app/routes/edit-category-tabs.js b/app/assets/javascripts/discourse/app/routes/edit-category-tabs.js index 07f8c7243d..64b18a9518 100644 --- a/app/assets/javascripts/discourse/app/routes/edit-category-tabs.js +++ b/app/assets/javascripts/discourse/app/routes/edit-category-tabs.js @@ -13,6 +13,7 @@ export default DiscourseRoute.extend({ controller.setProperties({ parentParams, selectedTab: transition.to.params.tab, + showTooltip: false, }); }, }); diff --git a/app/assets/javascripts/discourse/app/routes/full-page-search.js b/app/assets/javascripts/discourse/app/routes/full-page-search.js index f628bb7998..3c4fe84715 100644 --- a/app/assets/javascripts/discourse/app/routes/full-page-search.js +++ b/app/assets/javascripts/discourse/app/routes/full-page-search.js @@ -30,7 +30,7 @@ export default DiscourseRoute.extend({ model(params) { const cached = getTransient("lastSearch"); - var args = { q: params.q }; + let args = { q: params.q }; if (params.context_id && !args.skip_context) { args.search_context = { type: params.context, diff --git a/app/assets/javascripts/discourse/app/routes/signup.js b/app/assets/javascripts/discourse/app/routes/signup.js index f46e3c544e..89a6a04c7a 100644 --- a/app/assets/javascripts/discourse/app/routes/signup.js +++ b/app/assets/javascripts/discourse/app/routes/signup.js @@ -5,7 +5,7 @@ const SignupRoute = buildStaticRoute("signup"); SignupRoute.reopen({ beforeModel() { - var canSignUp = this.controllerFor("application").get("canSignUp"); + let canSignUp = this.controllerFor("application").get("canSignUp"); if (!this.siteSettings.login_required) { this.replaceWith("discovery.latest").then((e) => { diff --git a/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs b/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs index a311180f53..454aa33103 100644 --- a/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs @@ -5,7 +5,9 @@ class="btn-primary resend"}} {{/unless}} -{{d-button action=editActivationEmail - label="login.change_email" - icon="pencil-alt" - class="edit-email"}} +{{#if canEditEmail}} + {{d-button action=editActivationEmail + label="login.change_email" + icon="pencil-alt" + class="edit-email"}} +{{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs new file mode 100644 index 0000000000..a30363826d --- /dev/null +++ b/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs @@ -0,0 +1,6 @@ +{{#if showPostLocalDate}} + {{#tap-tile icon="globe-americas" tileId=tileId activeTile=activeTile onChange=(action "setReminder")}} +
{{i18n "bookmarks.reminders.post_local_date"}}
+
{{postLocalDateFormatted}}
+ {{/tap-tile}} +{{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/categories-with-featured-topics.hbs b/app/assets/javascripts/discourse/app/templates/components/categories-with-featured-topics.hbs index 9243d15028..b62e4a8fb5 100644 --- a/app/assets/javascripts/discourse/app/templates/components/categories-with-featured-topics.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/categories-with-featured-topics.hbs @@ -1,3 +1,2 @@ {{categories-only categories=categories - latestTopicOnly=latestTopicOnly showTopics="true"}} diff --git a/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs b/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs index b676b34e12..dc314151b3 100644 --- a/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs @@ -8,7 +8,7 @@ {{#if title}}
-

{{title}}

+

{{title}}

{{#if subtitle}}

{{subtitle}}

diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs index 353f1be130..60f5047be7 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs @@ -71,7 +71,7 @@
{{i18n "category.foreground_color"}}: -
+
#{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}} {{color-picker colors=foregroundColors value=category.text_color id="edit-text-color"}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/featured-topic.hbs b/app/assets/javascripts/discourse/app/templates/components/featured-topic.hbs index 937d05c1d5..c6a04a2d69 100644 --- a/app/assets/javascripts/discourse/app/templates/components/featured-topic.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/featured-topic.hbs @@ -2,11 +2,4 @@ {{html-safe topic.fancyTitle}} {{topic-post-badges newPosts=topic.totalUnread unseen=topic.unseen url=topic.lastUnreadUrl}} -{{#if latestTopicOnly}} - -{{else}} - {{format-age topic.last_posted_at}} -{{/if}} +{{format-age topic.last_posted_at}} diff --git a/app/assets/javascripts/discourse/app/templates/components/ignored-user-list.hbs b/app/assets/javascripts/discourse/app/templates/components/ignored-user-list.hbs index 73e122024d..44f88763ec 100644 --- a/app/assets/javascripts/discourse/app/templates/components/ignored-user-list.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/ignored-user-list.hbs @@ -6,4 +6,4 @@ {{/each}}
{{i18n "user.ignored_users_instructions"}}
-
{{#d-button action=(action "newIgnoredUser") class="btn-default" icon="plus"}}{{i18n "user.user_notifications.add_ignored_user"}}{{/d-button}}
+
{{d-button action=(action "newIgnoredUser") class="btn-default" icon="plus" label="user.user_notifications.add_ignored_user"}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs b/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs index 58e4b1a693..dbf5cc2922 100644 --- a/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs @@ -32,7 +32,7 @@ {{#if showTopics}} {{#each category.featuredTopics as |t|}} - {{featured-topic topic=t latestTopicOnly=latestTopicOnly}} + {{featured-topic topic=t}} {{/each}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs index c717b704d2..03b0a9a4d8 100644 --- a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs @@ -7,6 +7,7 @@
{{user-chooser + id="search-posted-by" value=searchedTerms.username onChange=(action "onChangeSearchTermForUsername") options=(hash @@ -20,6 +21,7 @@
{{search-advanced-category-chooser + id="search-in-category" value=searchedTerms.category.id onChange=(action "onChangeSearchTermForCategory") }} @@ -33,6 +35,7 @@
{{tag-chooser + id="search-with-tags" tags=searchedTerms.tags allowCreate=false everyTag=true @@ -57,66 +60,76 @@
-
- {{#if currentUser}} -
-
- {{/if}} - {{combo-box - id="in" - valueProperty="value" - content=inOptions - value=searchedTerms.in - onChange=(action "onChangeSearchTermForIn") - options=(hash - none="user.locale.any" - clearable=true - ) - }} + +
+ {{/if}} + {{combo-box + id="in" + valueProperty="value" + content=inOptions + value=searchedTerms.in + onChange=(action "onChangeSearchTermForIn") + options=(hash + none="user.locale.any" + clearable=true + ) + }} +
{{combo-box - id="status" + id="search-status-options" valueProperty="value" content=statusOptions value=searchedTerms.status @@ -179,7 +192,7 @@
- +
{{input diff --git a/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs b/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs index 38a950ba19..76545e5803 100644 --- a/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs @@ -89,16 +89,18 @@
- {{d-button - class="btn-default" - action=(action "save") - disabled=savingDisabled - label="tagging.groups.save"}} +
+ {{d-button + class="btn-default" + action=(action "save") + disabled=savingDisabled + label="tagging.groups.save"}} - {{d-button - class="btn-danger" - action=(action "destroy") - disabled=buffered.isNew - icon="far-trash-alt" - label="tagging.groups.delete"}} + {{d-button + class="btn-danger" + action=(action "destroy") + disabled=buffered.isNew + icon="far-trash-alt" + label="tagging.groups.delete"}} +
diff --git a/app/assets/javascripts/discourse/app/templates/discovery/categories.hbs b/app/assets/javascripts/discourse/app/templates/discovery/categories.hbs index 6a5c91b8b8..282c608b75 100644 --- a/app/assets/javascripts/discourse/app/templates/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/app/templates/discovery/categories.hbs @@ -1,6 +1,5 @@ {{#discovery-categories refresh=(action "refresh")}} {{component categoryPageStyle categories=model.categories - latestTopicOnly=latestTopicOnly topics=model.topics}} {{/discovery-categories}} diff --git a/app/assets/javascripts/discourse/app/templates/edit-category-tabs.hbs b/app/assets/javascripts/discourse/app/templates/edit-category-tabs.hbs index 4a2b132ccc..b5a7afc0cc 100644 --- a/app/assets/javascripts/discourse/app/templates/edit-category-tabs.hbs +++ b/app/assets/javascripts/discourse/app/templates/edit-category-tabs.hbs @@ -34,6 +34,12 @@ {{/each}}
+ {{#if showDeleteReason}} +
+

{{html-safe model.cannot_delete_reason}}

+
+ {{/if}} + {{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs index fcbd8c506d..c836ab9e16 100644 --- a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs @@ -203,9 +203,9 @@ {{i18n "search.advanced.title"}}
{{else}} - +

{{i18n "search.advanced.title"}} - +

{{/if}} {{#if site.mobileView}} diff --git a/app/assets/javascripts/discourse/app/templates/mobile/components/categories-with-featured-topics.hbs b/app/assets/javascripts/discourse/app/templates/mobile/components/categories-with-featured-topics.hbs index 9243d15028..b62e4a8fb5 100644 --- a/app/assets/javascripts/discourse/app/templates/mobile/components/categories-with-featured-topics.hbs +++ b/app/assets/javascripts/discourse/app/templates/mobile/components/categories-with-featured-topics.hbs @@ -1,3 +1,2 @@ {{categories-only categories=categories - latestTopicOnly=latestTopicOnly showTopics="true"}} diff --git a/app/assets/javascripts/discourse/app/templates/mobile/discovery/categories.hbs b/app/assets/javascripts/discourse/app/templates/mobile/discovery/categories.hbs index 6a5c91b8b8..282c608b75 100644 --- a/app/assets/javascripts/discourse/app/templates/mobile/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/app/templates/mobile/discovery/categories.hbs @@ -1,6 +1,5 @@ {{#discovery-categories refresh=(action "refresh")}} {{component categoryPageStyle categories=model.categories - latestTopicOnly=latestTopicOnly topics=model.topics}} {{/discovery-categories}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs index ba19e49d42..46049bb9b7 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs @@ -65,12 +65,7 @@
{{i18n "bookmarks.reminders.next_month"}}
{{nextMonthFormatted}}
{{/tap-tile}} - {{#if showPostLocalDate}} - {{#tap-tile icon="globe-americas" tileId=reminderTypes.POST_LOCAL_DATE activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.post_local_date"}}
-
{{postLocalDateFormatted}}
- {{/tap-tile}} - {{/if}} + {{bookmark-local-date postId=model.postId tileId=reminderTypes.POST_LOCAL_DATE activeTile=grid.activeTile onChange=(action "selectPostLocalDate")}} {{#tap-tile icon="calendar-alt" tileId=reminderTypes.CUSTOM activeTile=grid.activeTile onChange=(action "selectReminderType")}}
{{i18n "bookmarks.reminders.custom"}}
{{/tap-tile}} diff --git a/app/assets/javascripts/discourse/app/templates/user.hbs b/app/assets/javascripts/discourse/app/templates/user.hbs index 5a3468539d..e02068af25 100644 --- a/app/assets/javascripts/discourse/app/templates/user.hbs +++ b/app/assets/javascripts/discourse/app/templates/user.hbs @@ -67,7 +67,7 @@ {{/if}} {{#if currentUser.staff}} -
  • {{d-icon "wrench"}}{{i18n "admin.user.show_admin_profile"}}
  • +
  • {{d-icon "wrench"}}{{i18n "admin.user.show_admin_profile"}}
  • {{/if}} {{plugin-outlet name="user-profile-controls" tagName="" diff --git a/app/assets/javascripts/discourse/app/widgets/avatar-flair.js b/app/assets/javascripts/discourse/app/widgets/avatar-flair.js index 5aafe34de9..450b8bb40f 100644 --- a/app/assets/javascripts/discourse/app/widgets/avatar-flair.js +++ b/app/assets/javascripts/discourse/app/widgets/avatar-flair.js @@ -29,7 +29,7 @@ createWidget("avatar-flair", { }, buildAttributes(attrs) { - var style = ""; + let style = ""; if (!this.isIcon(attrs)) { style += "background-image: url(" + diff --git a/app/assets/javascripts/discourse/app/widgets/button.js b/app/assets/javascripts/discourse/app/widgets/button.js index 413d01ba28..01df29e6cf 100644 --- a/app/assets/javascripts/discourse/app/widgets/button.js +++ b/app/assets/javascripts/discourse/app/widgets/button.js @@ -31,13 +31,28 @@ export const ButtonClass = { buildAttributes() { const attrs = this.attrs; const attributes = {}; + let title = attrs.translatedTitle; - if (attrs.title) { - const title = I18n.t(attrs.title, attrs.titleOptions); + if (!title && attrs.title) { + title = I18n.t(attrs.title, attrs.titleOptions); + } + + if (title) { attributes["aria-label"] = title; attributes.title = title; } + if (attrs.role) { + attributes["role"] = attrs.role; + } + + if (attrs.tabAttrs) { + const tab = attrs.tabAttrs; + attributes["aria-selected"] = tab["aria-selected"]; + attributes["tabindex"] = tab["tabindex"]; + attributes["aria-controls"] = tab["aria-controls"]; + } + if (attrs.disabled) { attributes.disabled = "true"; } @@ -51,22 +66,40 @@ export const ButtonClass = { return attributes; }, + _buildIcon(attrs) { + const icon = iconNode(attrs.icon, { class: attrs.iconClass }); + if (attrs["aria-label"]) { + icon.properties.attributes["role"] = "img"; + icon.properties.attributes["aria-hidden"] = false; + } + return icon; + }, + html(attrs) { const contents = []; const left = !attrs.iconRight; if (attrs.icon && left) { - contents.push(iconNode(attrs.icon, { class: attrs.iconClass })); + contents.push(this._buildIcon(attrs)); } if (attrs.label) { contents.push( h("span.d-button-label", I18n.t(attrs.label, attrs.labelOptions)) ); } + if (attrs.translatedLabel) { + contents.push( + h( + "span.d-button-label", + attrs.translatedLabel.toString(), + attrs.translatedLabelOptions + ) + ); + } if (attrs.contents) { contents.push(attrs.contents); } if (attrs.icon && !left) { - contents.push(iconNode(attrs.icon, { class: attrs.iconClass })); + contents.push(this._buildIcon(attrs)); } return contents; diff --git a/app/assets/javascripts/discourse/app/widgets/header.js b/app/assets/javascripts/discourse/app/widgets/header.js index f8747fa552..e581b7d752 100644 --- a/app/assets/javascripts/discourse/app/widgets/header.js +++ b/app/assets/javascripts/discourse/app/widgets/header.js @@ -146,8 +146,10 @@ createWidget( "a.icon", { attributes: { - href: attrs.user.get("path"), - title: attrs.user.get("name"), + "aria-haspopup": true, + "aria-expanded": attrs.active, + href: attrs.user.path, + title: attrs.user.name || attrs.user.username, "data-auto-route": true, }, }, @@ -177,6 +179,8 @@ createWidget( "a.icon.btn-flat", { attributes: { + "aria-expanded": attrs.active, + "aria-haspopup": true, href: attrs.href, "data-auto-route": true, title, @@ -441,7 +445,7 @@ export default createWidget("header", { if (this.site.mobileView) { const searchService = this.register.lookup("search-service:main"); const context = searchService.get("searchContext"); - var params = ""; + let params = ""; if (context) { params = `?context=${context.type}&context_id=${context.id}&skip_context=${this.state.skipSearchContext}`; diff --git a/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js b/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js index 818da0e0c3..8d4e4c8673 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js +++ b/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js @@ -1,7 +1,5 @@ import I18n from "I18n"; import { createWidget } from "discourse/widgets/widget"; -import { h } from "virtual-dom"; -import { iconNode } from "discourse-common/lib/icon-library"; import { longDate } from "discourse/lib/formatter"; function mult(val) { @@ -53,24 +51,16 @@ export default createWidget("post-edits-indicator", { title = `${I18n.t("post.last_edited_on")} ${date}`; } - const contents = [ - attrs.version > 1 ? attrs.version - 1 : "", - " ", - iconNode(icon), - ]; - - return h( - "a", - { - className, - attributes: { title, href: "#" }, - }, - contents - ); + return this.attach("flat-button", { + icon, + translatedTitle: title, + className, + action: "onPostEditsIndicatorClick", + translatedLabel: attrs.version > 1 ? attrs.version - 1 : "", + }); }, - click(e) { - e.preventDefault(); + onPostEditsIndicatorClick() { if (this.attrs.wiki && this.attrs.version === 1) { this.sendWidgetAction("editPost"); } else if (this.attrs.canViewEditHistory) { diff --git a/app/assets/javascripts/discourse/app/widgets/post-small-action.js b/app/assets/javascripts/discourse/app/widgets/post-small-action.js index 700ffef23a..be5066e3d8 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-small-action.js +++ b/app/assets/javascripts/discourse/app/widgets/post-small-action.js @@ -12,7 +12,7 @@ export function actionDescriptionHtml(actionCode, createdAt, username) { const dt = new Date(createdAt); const when = autoUpdatingRelativeAge(dt, { format: "medium-with-ago" }); - var who = ""; + let who = ""; if (username) { if (actionCode === "invited_group" || actionCode === "removed_group") { who = `@${username}`; diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js index e90befbc42..5c7dad44c2 100644 --- a/app/assets/javascripts/discourse/app/widgets/post.js +++ b/app/assets/javascripts/discourse/app/widgets/post.js @@ -660,8 +660,10 @@ createWidget("post-article", { .find("post-reply-history", { postId: this.attrs.id }) .then((posts) => { this.state.repliesAbove = posts.map((p) => { - p.shareUrl = `${topicUrl}/${p.post_number}`; - return transformWithCallbacks(p); + let result = transformWithCallbacks(p); + result.shareUrl = `${topicUrl}/${p.post_number}`; + result.asPost = this.store.createRecord("post", p); + return result; }); }); } diff --git a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js index 23145723b5..8c6f0996f2 100644 --- a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js +++ b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js @@ -40,6 +40,15 @@ export default createWidget("quick-access-panel", { return Promise.resolve([]); }, + buildAttributes() { + const attributes = this.attrs; + attributes["aria-labelledby"] = this.key; + attributes["tabindex"] = "0"; + attributes["role"] = "tabpanel"; + + return attributes; + }, + newItemsLoaded() {}, itemHtml(item) {}, // eslint-disable-line no-unused-vars diff --git a/app/assets/javascripts/discourse/app/widgets/user-menu.js b/app/assets/javascripts/discourse/app/widgets/user-menu.js index e532b38a21..7c89f3da0c 100644 --- a/app/assets/javascripts/discourse/app/widgets/user-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/user-menu.js @@ -23,51 +23,82 @@ export function addUserMenuGlyph(glyph) { createWidget("user-menu-links", { tagName: "div.menu-links-header", + _tabAttrs(quickAccessType) { + return { + "aria-controls": `quick-access-${quickAccessType}`, + "aria-selected": "false", + tabindex: "-1", + }; + }, + + // TODO: Remove when 2.7 gets released. + _structureAsTab(extraGlyph) { + const glyph = extraGlyph; + // Assume glyph is a button if it has a data-url field. + if (!glyph.data || !glyph.data.url) { + glyph.title = glyph.label; + glyph.data = { url: glyph.href }; + + glyph.label = null; + glyph.href = null; + } + + glyph.role = "tab"; + glyph.tabAttrs = this._tabAttrs(glyph.actionParam); + + return glyph; + }, + profileGlyph() { return { - label: "user.preferences", + title: "user.preferences", className: "user-preferences-link", icon: "user", - href: `${this.attrs.path}/summary`, action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.PROFILE, - "aria-label": "user.preferences", + data: { url: `${this.attrs.path}/summary` }, + role: "tab", + tabAttrs: this._tabAttrs(QuickAccess.PROFILE), }; }, notificationsGlyph() { return { - label: "user.notifications", + title: "user.notifications", className: "user-notifications-link", icon: "bell", - href: `${this.attrs.path}/notifications`, action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.NOTIFICATIONS, - "aria-label": "user.notifications", + data: { url: `${this.attrs.path}/notifications` }, + role: "tab", + tabAttrs: this._tabAttrs(QuickAccess.NOTIFICATIONS), }; }, bookmarksGlyph() { return { + title: "user.bookmarks", action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.BOOKMARKS, - label: "user.bookmarks", className: "user-bookmarks-link", icon: "bookmark", - href: `${this.attrs.path}/activity/bookmarks`, + data: { url: `${this.attrs.path}/activity/bookmarks` }, "aria-label": "user.bookmarks", + role: "tab", + tabAttrs: this._tabAttrs(QuickAccess.BOOKMARKS), }; }, messagesGlyph() { return { + title: "user.private_messages", action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.MESSAGES, - label: "user.private_messages", className: "user-pms-link", icon: "envelope", - href: `${this.attrs.path}/messages`, - "aria-label": "user.private_messages", + data: { url: `${this.attrs.path}/messages` }, + role: "tab", + tabAttrs: this._tabAttrs(QuickAccess.MESSAGES), }; }, @@ -82,7 +113,7 @@ createWidget("user-menu-links", { if (this.isActive(glyph)) { glyph = this.markAsActive(glyph); } - return this.attach("link", $.extend(glyph, { hideLabel: true })); + return this.attach("flat-button", glyph); }, html() { @@ -94,7 +125,8 @@ createWidget("user-menu-links", { g = g(this); } if (g) { - glyphs.push(g); + const structuredGlyph = this._structureAsTab(g); + glyphs.push(structuredGlyph); } }); } @@ -108,9 +140,10 @@ createWidget("user-menu-links", { glyphs.push(this.profileGlyph()); - return h("ul.menu-links-row", [ + return h("div.menu-links-row", [ h( - "li.glyphs", + "div.glyphs", + { attributes: { "aria-label": "Menu links", role: "tablist" } }, glyphs.map((l) => this.glyphHtml(l)) ), ]); @@ -121,6 +154,7 @@ createWidget("user-menu-links", { // the full page. definition.action = null; definition.actionParam = null; + definition.url = definition.data.url; if (definition.className) { definition.className += " active"; @@ -128,6 +162,9 @@ createWidget("user-menu-links", { definition.className = "active"; } + definition.tabAttrs["tabindex"] = "0"; + definition.tabAttrs["aria-selected"] = "true"; + return definition; }, diff --git a/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js b/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js index 3df14fd12e..7393d1dbb5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js @@ -27,7 +27,7 @@ acceptance("Category Edit", function (needs) { await fillIn("input.category-name", "testing"); assert.equal(queryAll(".badge-category").text(), "testing"); - await fillIn("#edit-text-color", "#ff0000"); + await fillIn(".edit-text-color input", "#ff0000"); await click(".edit-category-topic-template"); await fillIn(".d-editor-input", "this is the new topic template"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index 1cec7ceb8c..1514688695 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -790,7 +790,7 @@ acceptance("Composer", function (needs) { await click( queryAll( ".button-wrapper[data-image-index='0'] .scale-btn[data-scale='50']" - ) + )[0] ); assertImageResized(assert, uploads); @@ -800,7 +800,7 @@ acceptance("Composer", function (needs) { await click( queryAll( ".button-wrapper[data-image-index='3'] .scale-btn[data-scale='50']" - ) + )[0] ); assertImageResized(assert, uploads); @@ -810,7 +810,7 @@ acceptance("Composer", function (needs) { await click( queryAll( ".button-wrapper[data-image-index='4'] .scale-btn[data-scale='75']" - ) + )[0] ); assertImageResized(assert, uploads); @@ -819,7 +819,7 @@ acceptance("Composer", function (needs) { await click( queryAll( ".button-wrapper[data-image-index='5'] .scale-btn[data-scale='50']" - ) + )[0] ); assertImageResized(assert, uploads); @@ -828,7 +828,7 @@ acceptance("Composer", function (needs) { await click( queryAll( ".button-wrapper[data-image-index='6'] .scale-btn[data-scale='75']" - ) + )[0] ); assertImageResized(assert, uploads); @@ -837,7 +837,7 @@ acceptance("Composer", function (needs) { await click( queryAll( ".button-wrapper[data-image-index='8'] .scale-btn[data-scale='75']" - ) + )[0] ); assertImageResized(assert, uploads); @@ -846,7 +846,7 @@ acceptance("Composer", function (needs) { await click( queryAll( ".button-wrapper[data-image-index='9'] .scale-btn[data-scale='75']" - ) + )[0] ); assertImageResized(assert, uploads); diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js index 9bbefe9313..c85cb272c5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js @@ -64,7 +64,7 @@ acceptance("Create Account - User Fields", function (needs) { await click(".modal-footer .btn-primary"); assert.equal(queryAll("#modal-alert")[0].style.display, ""); - await fillIn(".user-field input[type=text]:first", "Barky"); + await fillIn(".user-field input[type=text]:nth-of-type(1)", "Barky"); await click(".user-field input[type=checkbox]"); await click(".modal-footer .btn-primary"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js b/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js index b573383066..ac67b052df 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js @@ -127,6 +127,13 @@ acceptance("Dashboard", function (needs) { "its set the value of the filter from the query params" ); }); + + test("new features", async function (assert) { + await visit("/admin"); + + assert.ok(exists(".dashboard-new-features")); + assert.ok(exists(".dashboard-new-features .new-features-release-notes")); + }); }); acceptance("Dashboard: dashboard_visible_tabs", function (needs) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js b/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js index eaf9ca4896..86ce29be17 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js @@ -3,9 +3,20 @@ import { queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; -import { click, visit } from "@ember/test-helpers"; +import { click, settled, visit } from "@ember/test-helpers"; import { test } from "qunit"; +async function catchAbortedTransition() { + try { + await visit("/u/eviltrout/summary"); + } catch (e) { + if (e.message !== "TransitionAborted") { + throw e; + } + } + await settled(); +} + acceptance("Enforce Second Factor", function (needs) { needs.user(); needs.pretender((server, helper) => { @@ -21,7 +32,7 @@ acceptance("Enforce Second Factor", function (needs) { await visit("/u/eviltrout/preferences/second-factor"); this.siteSettings.enforce_second_factor = "staff"; - await visit("/u/eviltrout/summary"); + await catchAbortedTransition(); assert.equal( queryAll(".control-label").text(), @@ -45,7 +56,7 @@ acceptance("Enforce Second Factor", function (needs) { await visit("/u/eviltrout/preferences/second-factor"); this.siteSettings.enforce_second_factor = "all"; - await visit("/u/eviltrout/summary"); + await catchAbortedTransition(); assert.equal( queryAll(".control-label").text(), @@ -70,7 +81,7 @@ acceptance("Enforce Second Factor", function (needs) { this.siteSettings.enforce_second_factor = "all"; this.siteSettings.allow_anonymous_posting = true; - await visit("/u/eviltrout/summary"); + await catchAbortedTransition(); assert.notEqual( queryAll(".control-label").text(), diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-test.js index 2e92868be1..5164cd920d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-test.js @@ -261,6 +261,18 @@ acceptance("Group - Authenticated", function (needs) { "Awesome Team", "it should display the group name" ); + + await click(".group-details-button button.btn-danger"); + + assert.equal( + queryAll(".bootbox .modal-body").html(), + I18n.t("admin.groups.delete_with_messages_confirm", { + count: 2, + }), + "it should warn about orphan messages" + ); + + await click(".modal-footer .btn-default"); }); test("Moderator Viewing Group", async function (assert) { @@ -280,7 +292,7 @@ acceptance("Group - Authenticated", function (needs) { await click(".group-add-members-modal .modal-close"); - const memberDropdown = selectKit(".group-member-dropdown:first"); + const memberDropdown = selectKit(".group-member-dropdown:nth-of-type(1)"); await memberDropdown.expand(); assert.equal( diff --git a/app/assets/javascripts/discourse/tests/acceptance/hamburger-menu-test.js b/app/assets/javascripts/discourse/tests/acceptance/hamburger-menu-test.js index 618315426e..d073b44942 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/hamburger-menu-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/hamburger-menu-test.js @@ -6,22 +6,23 @@ import { import { click, visit } from "@ember/test-helpers"; import { test } from "qunit"; -acceptance("Opening the hamburger menu with some reviewables", function ( - needs -) { - needs.user(); - needs.pretender((server, helper) => { - server.get("/review/count.json", () => helper.response({ count: 3 })); - }); - test("As a staff member", async function (assert) { - updateCurrentUser({ moderator: true, admin: false }); +acceptance( + "Opening the hamburger menu with some reviewables", + function (needs) { + needs.user(); + needs.pretender((server, helper) => { + server.get("/review/count.json", () => helper.response({ count: 3 })); + }); + test("As a staff member", async function (assert) { + updateCurrentUser({ moderator: true, admin: false }); - await visit("/"); - await click(".hamburger-dropdown"); + await visit("/"); + await click(".hamburger-dropdown"); - assert.equal( - queryAll(".review .badge-notification.reviewables").text(), - "3" - ); - }); -}); + assert.equal( + queryAll(".review .badge-notification.reviewables").text(), + "3" + ); + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js b/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js index a03c92e3a4..814e727756 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js @@ -59,7 +59,7 @@ acceptance("Accept Invite - User Fields", function (needs) { "submit is still disabled due to lack of user fields" ); - await fillIn(".user-field input[type=text]:first", "Barky"); + await fillIn(".user-field input[type=text]:nth-of-type(1)", "Barky"); assert.ok( exists(".invites-show .btn-primary:disabled"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js b/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js index 4a517fae5b..42bacdd5f2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js @@ -1,4 +1,4 @@ -import { triggerKeyEvent, visit } from "@ember/test-helpers"; +import { getApplication, triggerKeyEvent, visit } from "@ember/test-helpers"; import KeyboardShortcutInitializer from "discourse/initializers/keyboard-shortcuts"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; import { acceptance } from "discourse/tests/helpers/qunit-helpers"; @@ -9,7 +9,7 @@ import { withPluginApi } from "discourse/lib/plugin-api"; acceptance("Plugin Keyboard Shortcuts - Logged In", function (needs) { needs.user(); needs.hooks.beforeEach(function () { - KeyboardShortcutInitializer.initialize(this.container); + KeyboardShortcutInitializer.initialize(getApplication()); }); test("a plugin can add a keyboard shortcut", async function (assert) { withPluginApi("0.8.38", (api) => { @@ -32,7 +32,7 @@ acceptance("Plugin Keyboard Shortcuts - Logged In", function (needs) { acceptance("Plugin Keyboard Shortcuts - Anonymous", function (needs) { needs.hooks.beforeEach(function () { - KeyboardShortcutInitializer.initialize(this.container); + KeyboardShortcutInitializer.initialize(getApplication()); }); test("a plugin can add a keyboard shortcut with an option", async function (assert) { let spy = sinon.spy(KeyboardShortcuts, "_bindToPath"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js index e22cfa1000..c87974cf1a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js @@ -95,18 +95,18 @@ acceptance("User Preferences", function (needs) { queryAll(".saved").remove(); }; - fillIn(".pref-name input[type=text]", "Jon Snow"); + await fillIn(".pref-name input[type=text]", "Jon Snow"); await savePreferences(); - click(".preferences-nav .nav-profile a"); - fillIn("#edit-location", "Westeros"); + await click(".preferences-nav .nav-profile a"); + await fillIn("#edit-location", "Westeros"); await savePreferences(); - click(".preferences-nav .nav-emails a"); - click(".pref-activity-summary input[type=checkbox]"); + await click(".preferences-nav .nav-emails a"); + await click(".pref-activity-summary input[type=checkbox]"); await savePreferences(); - click(".preferences-nav .nav-notifications a"); + await click(".preferences-nav .nav-notifications a"); await selectKit( ".control-group.notifications .combo-box.duration" ).expand(); @@ -115,8 +115,8 @@ acceptance("User Preferences", function (needs) { ).selectRowByValue(1440); await savePreferences(); - click(".preferences-nav .nav-categories a"); - fillIn(".tracking-controls .category-selector", "faq"); + await click(".preferences-nav .nav-categories a"); + await fillIn(".tracking-controls .category-selector input", "faq"); await savePreferences(); assert.ok( @@ -124,8 +124,8 @@ acceptance("User Preferences", function (needs) { "tags tab isn't there when tags are disabled" ); - click(".preferences-nav .nav-interface a"); - click(".control-group.other input[type=checkbox]:first"); + await click(".preferences-nav .nav-interface a"); + await click(".control-group.other input[type=checkbox]:nth-of-type(1)"); savePreferences(); assert.ok( @@ -175,15 +175,19 @@ acceptance("User Preferences", function (needs) { "it has the connected accounts section" ); assert.ok( - queryAll(".pref-associated-accounts table tr:first td:first") + queryAll( + ".pref-associated-accounts table tr:nth-of-type(1) td:nth-of-type(1)" + ) .html() .indexOf("Facebook") > -1, "it lists facebook" ); - await click(".pref-associated-accounts table tr:first td:last button"); + await click( + ".pref-associated-accounts table tr:nth-of-type(1) td:last-child button" + ); - queryAll(".pref-associated-accounts table tr:first td:last button") + queryAll(".pref-associated-accounts table tr:nth-of-type(1) td:last button") .html() .indexOf("Connect") > -1; }); @@ -286,28 +290,29 @@ acceptance("Second Factor Backups", function (needs) { }); }); -acceptance("Avatar selector when selectable avatars is enabled", function ( - needs -) { - needs.user(); - needs.settings({ selectable_avatars_enabled: true }); - needs.pretender((server, helper) => { - server.get("/site/selectable-avatars.json", () => - helper.response([ - "https://www.discourse.org", - "https://meta.discourse.org", - ]) - ); - }); +acceptance( + "Avatar selector when selectable avatars is enabled", + function (needs) { + needs.user(); + needs.settings({ selectable_avatars_enabled: true }); + needs.pretender((server, helper) => { + server.get("/site/selectable-avatars.json", () => + helper.response([ + "https://www.discourse.org", + "https://meta.discourse.org", + ]) + ); + }); - test("selectable avatars", async function (assert) { - await visit("/u/eviltrout/preferences"); - await click(".pref-avatar .btn"); - assert.ok( - exists(".selectable-avatars", "opens the avatar selection modal") - ); - }); -}); + test("selectable avatars", async function (assert) { + await visit("/u/eviltrout/preferences"); + await click(".pref-avatar .btn"); + assert.ok( + exists(".selectable-avatars", "opens the avatar selection modal") + ); + }); + } +); acceptance("User Preferences when badges are disabled", function (needs) { needs.user(); @@ -329,7 +334,7 @@ acceptance("User Preferences when badges are disabled", function (needs) { await visit("/u/eviltrout/preferences"); assert.equal( - queryAll(".auth-tokens > .auth-token:first .auth-token-device") + queryAll(".auth-tokens > .auth-token:nth-of-type(1) .auth-token-device") .text() .trim(), "Linux Computer", @@ -337,7 +342,7 @@ acceptance("User Preferences when badges are disabled", function (needs) { ); assert.equal( - queryAll(".pref-auth-tokens > a:first").text().trim(), + queryAll(".pref-auth-tokens > a:nth-of-type(1)").text().trim(), I18n.t("user.auth_tokens.show_all", { count: 3 }), "it should display two tokens" ); @@ -346,14 +351,14 @@ acceptance("User Preferences when badges are disabled", function (needs) { "it should display two tokens" ); - await click(".pref-auth-tokens > a:first"); + await click(".pref-auth-tokens > a:nth-of-type(1)"); assert.ok( queryAll(".pref-auth-tokens .auth-token").length === 3, "it should display three tokens" ); - await click(".auth-token-dropdown:first button"); + await click(".auth-token-dropdown button:nth-of-type(1)"); await click("li[data-value='notYou']"); assert.ok(queryAll(".d-modal:visible").length === 1, "modal should appear"); @@ -392,7 +397,9 @@ acceptance( "clear button not present" ); - const selectTopicBtn = queryAll(".feature-topic-on-profile-btn:first"); + const selectTopicBtn = queryAll( + ".feature-topic-on-profile-btn:nth-of-type(1)" + )[0]; assert.ok(exists(selectTopicBtn), "feature topic button is present"); await click(selectTopicBtn); @@ -402,7 +409,9 @@ acceptance( "topic picker modal is open" ); - const topicRadioBtn = queryAll('input[name="choose_topic_id"]:first'); + const topicRadioBtn = queryAll( + 'input[name="choose_topic_id"]:nth-of-type(1)' + )[0]; assert.ok(exists(topicRadioBtn), "Topic options are prefilled"); await click(topicRadioBtn); diff --git a/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js b/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js index 13a2f52e1e..24d8055fb7 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js @@ -3,7 +3,7 @@ import { addRawTemplate, removeRawTemplate, } from "discourse-common/lib/raw-templates"; -import compile from "handlebars-compiler"; +import { compile } from "handlebars"; import { test } from "qunit"; import { visit } from "@ember/test-helpers"; diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js index de8eaab54d..48f0d1f9ed 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js @@ -160,7 +160,9 @@ acceptance("Search - Full Page", function (needs) { '"autocomplete" popup has an entry for "admin"' ); - await click(".search-advanced-options .autocomplete ul li a:first"); + await click( + ".search-advanced-options .autocomplete ul li a:nth-of-type(1)" + ); assert.ok( exists('.search-advanced-options span:contains("admin")'), @@ -308,7 +310,7 @@ acceptance("Search - Full Page", function (needs) { test("update status through advanced search ui", async function (assert) { const statusSelector = selectKit( - ".search-advanced-options .select-kit#status" + ".search-advanced-options .select-kit#search-status-options" ); await visit("/search"); @@ -331,7 +333,7 @@ acceptance("Search - Full Page", function (needs) { test("doesn't update status filter header if wrong value entered through searchbox", async function (assert) { const statusSelector = selectKit( - ".search-advanced-options .select-kit#status" + ".search-advanced-options .select-kit#search-status-options" ); await visit("/search"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js index 62df079754..9b6136663a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js @@ -154,10 +154,6 @@ acceptance("Signing In", function () { "the username validation is bad" ); await click(".modal-footer .btn-primary"); - assert.ok( - exists("#new-account-username:focus"), - "username field is focused" - ); await fillIn("#new-account-username", "goodtuna"); assert.ok( diff --git a/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js b/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js index c3ab9d85d8..6d8de06da1 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js @@ -46,7 +46,7 @@ acceptance("Tag Groups", function (needs) { await click(".tag-group-content .btn.btn-default"); - await click(".tag-chooser .choice:first"); + await click(".tag-chooser .choice:nth-of-type(1)"); assert.ok(!queryAll(".tag-group-content .btn.btn-danger")[0].disabled); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js index a3d339afec..8d0670b0c8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js @@ -1,6 +1,7 @@ import { acceptance, exists, + invisible, queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; @@ -10,6 +11,74 @@ import { test } from "qunit"; acceptance("Tags", function (needs) { needs.user(); + needs.pretender((server, helper) => { + server.get("/tag/test/notifications", () => + helper.response({ + tag_notification: { id: "test", notification_level: 2 }, + }) + ); + + server.get("/tag/test/l/unread.json", () => + helper.response({ + users: [ + { + id: 42, + username: "foo", + name: "Foo", + avatar_template: "/user_avatar/localhost/foo/{size}/10265_2.png", + }, + ], + primary_groups: [], + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + per_page: 30, + top_tags: [], + tags: [{ id: 42, name: "test", topic_count: 1, staff: false }], + topics: [ + { + id: 42, + title: "Hello world", + fancy_title: "Hello world", + slug: "hello-world", + posts_count: 1, + reply_count: 1, + highest_post_number: 1, + created_at: "2020-01-01T00:00:00.000Z", + last_posted_at: "2020-01-01T00:00:00.000Z", + bumped: true, + bumped_at: "2020-01-01T00:00:00.000Z", + archetype: "regular", + unseen: false, + last_read_post_number: 1, + unread: 0, + new_posts: 1, + pinned: false, + unpinned: null, + visible: true, + closed: true, + archived: false, + notification_level: 3, + bookmarked: false, + liked: true, + tags: ["test"], + views: 42, + like_count: 42, + has_summary: false, + last_poster_username: "foo", + pinned_globally: false, + featured_link: null, + posters: [], + }, + ], + }, + }) + ); + + server.put("/topics/bulk", () => helper.response({})); + }); + test("list the tags", async function (assert) { await visit("/tags"); @@ -19,6 +88,13 @@ acceptance("Tags", function (needs) { "shows the eviltrout tag" ); }); + + test("dismiss notifications", async function (assert) { + await visit("/tag/test/l/unread"); + await click("button.dismiss-read"); + await click(".dismiss-read-modal button.btn-primary"); + assert.ok(invisible(".dismiss-read-modal")); + }); }); acceptance("Tags listed by group", function (needs) { @@ -102,7 +178,7 @@ acceptance("Tags listed by group", function (needs) { "shown in given order and with tags that are not in a group" ); assert.deepEqual( - $(".tag-list:first .discourse-tag") + $(".tag-list:nth-of-type(1) .discourse-tag") .toArray() .map((i) => { return $(i).text(); @@ -111,7 +187,7 @@ acceptance("Tags listed by group", function (needs) { "shows the tags in default sort (by count)" ); assert.deepEqual( - $(".tag-list:first .discourse-tag") + $(".tag-list:nth-of-type(1) .discourse-tag") .toArray() .map((i) => { return $(i).attr("href"); @@ -302,7 +378,7 @@ acceptance("Tag info", function (needs) { "delete UI is visible" ); - await click(".unlink-synonym:first"); + await click(".unlink-synonym:nth-of-type(1)"); assert.ok( queryAll(".tag-info .synonyms-list .tag-box").length === 1, "removed a synonym" diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js index 627da72912..f71e25020d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js @@ -22,7 +22,7 @@ acceptance("Topic Discovery", function (needs) { assert.ok(exists(".topic-list .topic-list-item"), "has topics"); assert.equal( - queryAll("a[data-user-card=eviltrout]:first img.avatar").attr("title"), + queryAll("a[data-user-card=eviltrout] img.avatar").attr("title"), "Evil Trout - Most Posts", "it shows user's full name in avatar title" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js index 351d39e573..29b8d6d8c2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js @@ -1,17 +1,22 @@ import { acceptance, - exists, queryAll, visible, } from "discourse/tests/helpers/qunit-helpers"; -import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; +import { + click, + fillIn, + settled, + triggerKeyEvent, + visit, +} from "@ember/test-helpers"; import I18n from "I18n"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import { test } from "qunit"; import { IMAGE_VERSION as v } from "pretty-text/emoji/version"; import { withPluginApi } from "discourse/lib/plugin-api"; -function selectText(selector) { +async function selectText(selector) { const range = document.createRange(); const node = document.querySelector(selector); range.selectNodeContents(node); @@ -19,6 +24,7 @@ function selectText(selector) { const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); + await settled(); } acceptance("Topic", function (needs) { @@ -140,7 +146,7 @@ acceptance("Topic", function (needs) { await click(".topic-post:nth-of-type(1) button.show-post-admin-menu"); await click(".btn.wiki"); - assert.ok(queryAll("a.wiki").length === 1, "it shows the wiki icon"); + assert.ok(queryAll("button.wiki").length === 1, "it shows the wiki icon"); }); test("Visit topic routes", async function (assert) { @@ -302,7 +308,7 @@ acceptance("Topic featured links", function (needs) { await click(".toggle-admin-menu"); await click(".topic-admin-pin .btn"); - await click(".btn-primary:last"); + await click(".make-banner"); await click(".toggle-admin-menu"); await click(".topic-admin-visible .btn"); @@ -362,7 +368,7 @@ acceptance("Topic featured links", function (needs) { test("Quoting a quote keeps the original poster name", async function (assert) { await visit("/t/internationalization-localization/280"); - selectText("#post_5 blockquote"); + await selectText("#post_5 blockquote"); await click(".quote-button .insert-quote"); assert.ok( @@ -374,7 +380,7 @@ acceptance("Topic featured links", function (needs) { test("Quoting a quote of a different topic keeps the original topic title", async function (assert) { await visit("/t/internationalization-localization/280"); - selectText("#post_9 blockquote"); + await selectText("#post_9 blockquote"); await click(".quote-button .insert-quote"); assert.ok( @@ -388,7 +394,7 @@ acceptance("Topic featured links", function (needs) { test("Quoting a quote with the Reply button keeps the original poster name", async function (assert) { await visit("/t/internationalization-localization/280"); - selectText("#post_5 blockquote"); + await selectText("#post_5 blockquote"); await click(".reply"); assert.ok( @@ -400,7 +406,7 @@ acceptance("Topic featured links", function (needs) { test("Quoting a quote with replyAsNewTopic keeps the original poster name", async function (assert) { await visit("/t/internationalization-localization/280"); - selectText("#post_5 blockquote"); + await selectText("#post_5 blockquote"); await triggerKeyEvent(document, "keypress", "j".charCodeAt(0)); await triggerKeyEvent(document, "keypress", "t".charCodeAt(0)); @@ -413,7 +419,7 @@ acceptance("Topic featured links", function (needs) { test("Quoting by selecting text can mark the quote as full", async function (assert) { await visit("/t/internationalization-localization/280"); - selectText("#post_5 .cooked"); + await selectText("#post_5 .cooked"); await click(".quote-button .insert-quote"); assert.ok( diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js index 3dff64d71d..2e9281f368 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js @@ -17,7 +17,7 @@ acceptance("User Card - Show Local Time", function (needs) { User.current().changeTimezone("Australia/Brisbane"); await visit("/t/internationalization-localization/280"); - await click('a[data-user-card="charlie"]:first'); + await click('a[data-user-card="charlie"]'); assert.not( exists(".user-card .local-time"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-test.js index 2c747253a6..ae7603bf4d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-test.js @@ -15,7 +15,13 @@ acceptance("User Routes", function (needs) { ); }); test("Invalid usernames", async function (assert) { - await visit("/u/eviltrout%2F..%2F..%2F/summary"); + try { + await visit("/u/eviltrout%2F..%2F..%2F/summary"); + } catch (e) { + if (e.message !== "TransitionAborted") { + throw e; + } + } assert.equal(currentRouteName(), "exception-unknown"); }); diff --git a/app/assets/javascripts/discourse/tests/fixtures/dashboard-new-features.js b/app/assets/javascripts/discourse/tests/fixtures/dashboard-new-features.js new file mode 100644 index 0000000000..fe9d50b791 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/fixtures/dashboard-new-features.js @@ -0,0 +1,32 @@ +export default { + "/admin/dashboard/new-features.json": { + new_features: [ + { + id: 1, + user_id: 127, + emoji: "😎", + title: "New color palettes", + description: + "New light and dark color palettes that adhere to Web Content Accessibility Guidelines. ", + tier: [], + link: "https://meta.discourse.org", + created_at: "2021-01-18T19:59:29.666Z", + updated_at: "2021-01-19T19:33:16.150Z", + }, + { + id: 7, + user_id: 127, + emoji: "👱‍♀️", + title: "Suspend users quickly", + description: + "Staff can now suspend or silence a user immediately, without needing to visit the review queue or admin page. ", + tier: [], + link: "", + created_at: "2021-01-19T19:20:09.757Z", + updated_at: "2021-01-19T19:20:09.757Z", + } + ], + release_notes_link: + "https://meta.discourse.org/c/feature/announcements?tags=release-notes\u0026before=0", + }, +}; diff --git a/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js index f1a7867be6..615fe75086 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js @@ -46,7 +46,9 @@ export default { is_group_owner: true, mentionable: true, messageable: true, - can_see_members: true + can_see_members: true, + has_messages: true, + message_count: 2 }, extras: { visible_group_names: ["discourse"] diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js index b75d641d33..80997d6f07 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js @@ -144,6 +144,15 @@ export function applyDefaultHandlers(pretender) { return response({ email: "eviltrout@example.com" }); }); + pretender.get("/u/is_local_username", () => + response({ + valid: [], + valid_groups: [], + mentionable_groups: [], + cannot_see: [], + }) + ); + pretender.get("/u/eviltrout.json", () => { const json = fixturesByUrl["/u/eviltrout.json"]; json.user.can_edit = loggedIn(); diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 7072dbea9a..948abcd31c 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -378,6 +378,10 @@ export function queryAll() { return window.find(...arguments); } +export function query() { + return document.querySelector(...arguments); +} + export function invisible(selector) { const $items = queryAll(selector + ":visible"); return ( diff --git a/app/assets/javascripts/discourse/tests/integration/components/activation-controls-test.js b/app/assets/javascripts/discourse/tests/integration/components/activation-controls-test.js new file mode 100644 index 0000000000..b97c72aecd --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/activation-controls-test.js @@ -0,0 +1,26 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { + discourseModule, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; + +discourseModule( + "Integration | Component | activation-controls", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("hides change email button", { + template: `{{activation-controls}}`, + beforeEach() { + this.siteSettings.enable_local_logins = false; + this.siteSettings.email_editable = false; + }, + + test(assert) { + assert.equal(queryAll("button.edit-email").length, 0); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js b/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js index 2503f89da9..c6408ba2aa 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js @@ -97,33 +97,33 @@ discourseModule("Integration | Component | d-editor", function (hooks) { }); } - testCase(`selecting the space before a word`, async function ( - assert, - textarea - ) { - textarea.selectionStart = 5; - textarea.selectionEnd = 7; + testCase( + `selecting the space before a word`, + async function (assert, textarea) { + textarea.selectionStart = 5; + textarea.selectionEnd = 7; - await click(`button.bold`); + await click(`button.bold`); - assert.equal(this.value, `hello **w**orld.`); - assert.equal(textarea.selectionStart, 8); - assert.equal(textarea.selectionEnd, 9); - }); + assert.equal(this.value, `hello **w**orld.`); + assert.equal(textarea.selectionStart, 8); + assert.equal(textarea.selectionEnd, 9); + } + ); - testCase(`selecting the space after a word`, async function ( - assert, - textarea - ) { - textarea.selectionStart = 0; - textarea.selectionEnd = 6; + testCase( + `selecting the space after a word`, + async function (assert, textarea) { + textarea.selectionStart = 0; + textarea.selectionEnd = 6; - await click(`button.bold`); + await click(`button.bold`); - assert.equal(this.value, `**hello** world.`); - assert.equal(textarea.selectionStart, 2); - assert.equal(textarea.selectionEnd, 7); - }); + assert.equal(this.value, `**hello** world.`); + assert.equal(textarea.selectionStart, 2); + assert.equal(textarea.selectionEnd, 7); + } + ); testCase(`bold button with no selection`, async function (assert, textarea) { await click(`button.bold`); @@ -149,37 +149,37 @@ discourseModule("Integration | Component | d-editor", function (hooks) { assert.equal(textarea.selectionEnd, 11); }); - testCase(`bold with a multiline selection`, async function ( - assert, - textarea - ) { - this.set("value", "hello\n\nworld\n\ntest."); + testCase( + `bold with a multiline selection`, + async function (assert, textarea) { + this.set("value", "hello\n\nworld\n\ntest."); - textarea.selectionStart = 0; - textarea.selectionEnd = 12; + textarea.selectionStart = 0; + textarea.selectionEnd = 12; - await click(`button.bold`); - assert.equal(this.value, `**hello**\n\n**world**\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 20); + await click(`button.bold`); + assert.equal(this.value, `**hello**\n\n**world**\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 20); - await click(`button.bold`); - assert.equal(this.value, `hello\n\nworld\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 12); - }); + await click(`button.bold`); + assert.equal(this.value, `hello\n\nworld\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 12); + } + ); - testCase(`italic button with no selection`, async function ( - assert, - textarea - ) { - await click(`button.italic`); - const example = I18n.t(`composer.italic_text`); - assert.equal(this.value, `hello world.*${example}*`); + testCase( + `italic button with no selection`, + async function (assert, textarea) { + await click(`button.italic`); + const example = I18n.t(`composer.italic_text`); + assert.equal(this.value, `hello world.*${example}*`); - assert.equal(textarea.selectionStart, 13); - assert.equal(textarea.selectionEnd, 13 + example.length); - }); + assert.equal(textarea.selectionStart, 13); + assert.equal(textarea.selectionEnd, 13 + example.length); + } + ); testCase(`italic button with a selection`, async function (assert, textarea) { textarea.selectionStart = 6; @@ -196,25 +196,25 @@ discourseModule("Integration | Component | d-editor", function (hooks) { assert.equal(textarea.selectionEnd, 11); }); - testCase(`italic with a multiline selection`, async function ( - assert, - textarea - ) { - this.set("value", "hello\n\nworld\n\ntest."); + testCase( + `italic with a multiline selection`, + async function (assert, textarea) { + this.set("value", "hello\n\nworld\n\ntest."); - textarea.selectionStart = 0; - textarea.selectionEnd = 12; + textarea.selectionStart = 0; + textarea.selectionEnd = 12; - await click(`button.italic`); - assert.equal(this.value, `*hello*\n\n*world*\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 16); + await click(`button.italic`); + assert.equal(this.value, `*hello*\n\n*world*\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 16); - await click(`button.italic`); - assert.equal(this.value, `hello\n\nworld\n\ntest.`); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 12); - }); + await click(`button.italic`); + assert.equal(this.value, `hello\n\nworld\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 12); + } + ); componentTest("advanced code", { template: "{{d-editor value=value}}", @@ -508,20 +508,20 @@ third line` assert.equal(this.value, "hello\n\nwor\n\nld.\n\n> Blockquote"); }); - testCase(`bullet button with no selection`, async function ( - assert, - textarea - ) { - const example = I18n.t("composer.list_item"); + testCase( + `bullet button with no selection`, + async function (assert, textarea) { + const example = I18n.t("composer.list_item"); - await click(`button.bullet`); - assert.equal(this.value, `hello world.\n\n* ${example}`); - assert.equal(textarea.selectionStart, 14); - assert.equal(textarea.selectionEnd, 16 + example.length); + await click(`button.bullet`); + assert.equal(this.value, `hello world.\n\n* ${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 16 + example.length); - await click(`button.bullet`); - assert.equal(this.value, `hello world.\n\n${example}`); - }); + await click(`button.bullet`); + assert.equal(this.value, `hello world.\n\n${example}`); + } + ); testCase(`bullet button with a selection`, async function (assert, textarea) { textarea.selectionStart = 6; @@ -538,25 +538,25 @@ third line` assert.equal(textarea.selectionEnd, 12); }); - testCase(`bullet button with a multiple line selection`, async function ( - assert, - textarea - ) { - this.set("value", "* Hello\n\nWorld\n\nEvil"); + testCase( + `bullet button with a multiple line selection`, + async function (assert, textarea) { + this.set("value", "* Hello\n\nWorld\n\nEvil"); - textarea.selectionStart = 0; - textarea.selectionEnd = 20; + textarea.selectionStart = 0; + textarea.selectionEnd = 20; - await click(`button.bullet`); - assert.equal(this.value, "Hello\n\nWorld\n\nEvil"); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 18); + await click(`button.bullet`); + assert.equal(this.value, "Hello\n\nWorld\n\nEvil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 18); - await click(`button.bullet`); - assert.equal(this.value, "* Hello\n\n* World\n\n* Evil"); - assert.equal(textarea.selectionStart, 0); - assert.equal(textarea.selectionEnd, 24); - }); + await click(`button.bullet`); + assert.equal(this.value, "* Hello\n\n* World\n\n* Evil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 24); + } + ); testCase(`list button with no selection`, async function (assert, textarea) { const example = I18n.t("composer.list_item"); @@ -633,23 +633,23 @@ third line` }, }); - testCase(`doesn't jump to bottom with long text`, async function ( - assert, - textarea - ) { - let longText = "hello world."; - for (let i = 0; i < 8; i++) { - longText = longText + longText; + testCase( + `doesn't jump to bottom with long text`, + async function (assert, textarea) { + let longText = "hello world."; + for (let i = 0; i < 8; i++) { + longText = longText + longText; + } + this.set("value", longText); + + $(textarea).scrollTop(0); + textarea.selectionStart = 3; + textarea.selectionEnd = 3; + + await click("button.bold"); + assert.equal($(textarea).scrollTop(), 0, "it stays scrolled up"); } - this.set("value", longText); - - $(textarea).scrollTop(0); - textarea.selectionStart = 3; - textarea.selectionEnd = 3; - - await click("button.bold"); - assert.equal($(textarea).scrollTop(), 0, "it stays scrolled up"); - }); + ); componentTest("emoji", { template: "{{d-editor value=value}}", diff --git a/app/assets/javascripts/discourse/tests/integration/components/date-time-input-range-test.js b/app/assets/javascripts/discourse/tests/integration/components/date-time-input-range-test.js index eade3cffdf..f7b2ca1af9 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/date-time-input-range-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/date-time-input-range-test.js @@ -24,23 +24,24 @@ function toTimeInput() { const DEFAULT_DATE_TIME = moment("2019-01-29 14:45"); -discourseModule("Integration | Component | date-time-input-range", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | date-time-input-range", + function (hooks) { + setupRenderingTest(hooks); - componentTest("default", { - template: `{{date-time-input-range from=from to=to}}`, + componentTest("default", { + template: `{{date-time-input-range from=from to=to}}`, - beforeEach() { - this.setProperties({ from: DEFAULT_DATE_TIME, to: null }); - }, + beforeEach() { + this.setProperties({ from: DEFAULT_DATE_TIME, to: null }); + }, - test(assert) { - assert.equal(fromDateInput().value, "January 29, 2019"); - assert.equal(fromTimeInput().dataset.name, "14:45"); - assert.equal(toDateInput().value, ""); - assert.equal(toTimeInput().dataset.name, "--:--"); - }, - }); -}); + test(assert) { + assert.equal(fromDateInput().value, "January 29, 2019"); + assert.equal(fromTimeInput().dataset.name, "14:45"); + assert.equal(toDateInput().value, ""); + assert.equal(toTimeInput().dataset.name, "--:--"); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/secret-value-list-test.js b/app/assets/javascripts/discourse/tests/integration/components/secret-value-list-test.js index 684ef966d1..bc3bc78a74 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/secret-value-list-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/secret-value-list-test.js @@ -8,98 +8,99 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import I18n from "I18n"; -discourseModule("Integration | Component | secret-value-list", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | secret-value-list", + function (hooks) { + setupRenderingTest(hooks); - componentTest("adding a value", { - template: "{{secret-value-list values=values}}", + componentTest("adding a value", { + template: "{{secret-value-list values=values}}", - async test(assert) { - this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); + async test(assert) { + this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); - await fillIn(".new-value-input.key", "thirdKey"); - await click(".add-value-btn"); + await fillIn(".new-value-input.key", "thirdKey"); + await click(".add-value-btn"); - assert.ok( - queryAll(".values .value").length === 2, - "it doesn't add the value to the list if secret is missing" - ); + assert.ok( + queryAll(".values .value").length === 2, + "it doesn't add the value to the list if secret is missing" + ); - await fillIn(".new-value-input.key", ""); - await fillIn(".new-value-input.secret", "thirdValue"); - await click(".add-value-btn"); + await fillIn(".new-value-input.key", ""); + await fillIn(".new-value-input.secret", "thirdValue"); + await click(".add-value-btn"); - assert.ok( - queryAll(".values .value").length === 2, - "it doesn't add the value to the list if key is missing" - ); + assert.ok( + queryAll(".values .value").length === 2, + "it doesn't add the value to the list if key is missing" + ); - await fillIn(".new-value-input.key", "thirdKey"); - await fillIn(".new-value-input.secret", "thirdValue"); - await click(".add-value-btn"); + await fillIn(".new-value-input.key", "thirdKey"); + await fillIn(".new-value-input.secret", "thirdValue"); + await click(".add-value-btn"); - assert.ok( - queryAll(".values .value").length === 3, - "it adds the value to the list of values" - ); + assert.ok( + queryAll(".values .value").length === 3, + "it adds the value to the list of values" + ); - assert.deepEqual( - this.values, - "firstKey|FirstValue\nsecondKey|secondValue\nthirdKey|thirdValue", - "it adds the value to the list of values" - ); - }, - }); + assert.deepEqual( + this.values, + "firstKey|FirstValue\nsecondKey|secondValue\nthirdKey|thirdValue", + "it adds the value to the list of values" + ); + }, + }); - componentTest("adding an invalid value", { - template: "{{secret-value-list values=values}}", + componentTest("adding an invalid value", { + template: "{{secret-value-list values=values}}", - async test(assert) { - await fillIn(".new-value-input.key", "someString"); - await fillIn(".new-value-input.secret", "keyWithAPipe|Hidden"); - await click(".add-value-btn"); + async test(assert) { + await fillIn(".new-value-input.key", "someString"); + await fillIn(".new-value-input.secret", "keyWithAPipe|Hidden"); + await click(".add-value-btn"); - assert.ok( - queryAll(".values .value").length === 0, - "it doesn't add the value to the list of values" - ); + assert.ok( + queryAll(".values .value").length === 0, + "it doesn't add the value to the list of values" + ); - assert.deepEqual( - this.values, - undefined, - "it doesn't add the value to the list of values" - ); + assert.deepEqual( + this.values, + undefined, + "it doesn't add the value to the list of values" + ); - assert.ok( - queryAll(".validation-error") - .html() - .indexOf(I18n.t("admin.site_settings.secret_list.invalid_input")) > - -1, - "it shows validation error" - ); - }, - }); + assert.ok( + queryAll(".validation-error") + .html() + .indexOf(I18n.t("admin.site_settings.secret_list.invalid_input")) > + -1, + "it shows validation error" + ); + }, + }); - componentTest("removing a value", { - template: "{{secret-value-list values=values}}", + componentTest("removing a value", { + template: "{{secret-value-list values=values}}", - async test(assert) { - this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); + async test(assert) { + this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); - await click(".values .value[data-index='0'] .remove-value-btn"); + await click(".values .value[data-index='0'] .remove-value-btn"); - assert.ok( - queryAll(".values .value").length === 1, - "it removes the value from the list of values" - ); + assert.ok( + queryAll(".values .value").length === 1, + "it removes the value from the list of values" + ); - assert.equal( - this.values, - "secondKey|secondValue", - "it removes the expected value" - ); - }, - }); -}); + assert.equal( + this.values, + "secondKey|secondValue", + "it removes the expected value" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js index 4cc1a4b013..b3611c3670 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js @@ -47,313 +47,314 @@ function template(options = []) { `; } -discourseModule("Integration | Component | select-kit/category-drop", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | select-kit/category-drop", + function (hooks) { + setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set("subject", selectKit()); - }); + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); - componentTest("caretUpIcon", { - template: ` + componentTest("caretUpIcon", { + template: ` {{category-drop category=value categories=content }} `, - async test(assert) { - const $header = this.subject.header().el(); + async test(assert) { + const $header = this.subject.header().el(); - assert.ok( - exists($header.find(`.d-icon-caret-right`)), - "it uses the correct default icon" - ); - }, - }); + assert.ok( + exists($header.find(`.d-icon-caret-right`)), + "it uses the correct default icon" + ); + }, + }); - componentTest("none", { - template: ` + componentTest("none", { + template: ` {{category-drop category=value categories=content }} `, - async test(assert) { - const text = this.subject.header().label(); - assert.equal( - text, - I18n.t("category.all").toLowerCase(), - "it uses the noneLabel" - ); - }, - }); + async test(assert) { + const text = this.subject.header().label(); + assert.equal( + text, + I18n.t("category.all").toLowerCase(), + "it uses the noneLabel" + ); + }, + }); - componentTest("[not staff - TL0] displayCategoryDescription", { - template: template(), + componentTest("[not staff - TL0] displayCategoryDescription", { + template: template(), - beforeEach() { - set(this.currentUser, "staff", false); - set(this.currentUser, "trust_level", 0); + beforeEach() { + set(this.currentUser, "staff", false); + set(this.currentUser, "trust_level", 0); - initCategories(this); - }, + initCategories(this); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const row = this.subject.rowByValue(this.category.id); - assert.ok( - exists(row.el().find(".category-desc")), - "it shows category description for newcomers" - ); - }, - }); + const row = this.subject.rowByValue(this.category.id); + assert.ok( + exists(row.el().find(".category-desc")), + "it shows category description for newcomers" + ); + }, + }); - componentTest("[not staff - TL1] displayCategoryDescription", { - template: template(), + componentTest("[not staff - TL1] displayCategoryDescription", { + template: template(), - beforeEach() { - set(this.currentUser, "moderator", false); - set(this.currentUser, "admin", false); - set(this.currentUser, "trust_level", 1); - initCategories(this); - }, + beforeEach() { + set(this.currentUser, "moderator", false); + set(this.currentUser, "admin", false); + set(this.currentUser, "trust_level", 1); + initCategories(this); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const row = this.subject.rowByValue(this.category.id); - assert.ok( - !exists(row.el().find(".category-desc")), - "it doesn't shows category description for TL0+" - ); - }, - }); + const row = this.subject.rowByValue(this.category.id); + assert.ok( + !exists(row.el().find(".category-desc")), + "it doesn't shows category description for TL0+" + ); + }, + }); - componentTest("[staff - TL0] displayCategoryDescription", { - template: template(), + componentTest("[staff - TL0] displayCategoryDescription", { + template: template(), - beforeEach() { - set(this.currentUser, "moderator", true); - set(this.currentUser, "trust_level", 0); + beforeEach() { + set(this.currentUser, "moderator", true); + set(this.currentUser, "trust_level", 0); - initCategories(this); - }, + initCategories(this); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const row = this.subject.rowByValue(this.category.id); - assert.ok( - !exists(row.el().find(".category-desc")), - "it doesn't show category description for staff" - ); - }, - }); + const row = this.subject.rowByValue(this.category.id); + assert.ok( + !exists(row.el().find(".category-desc")), + "it doesn't show category description for staff" + ); + }, + }); - componentTest("hideParentCategory (default: false)", { - template: template(), + componentTest("hideParentCategory (default: false)", { + template: template(), - beforeEach() { - initCategories(this); - }, + beforeEach() { + initCategories(this); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const row = this.subject.rowByValue(this.category.id); - assert.equal(row.value(), this.category.id); - assert.equal(this.category.parent_category_id, null); - }, - }); + const row = this.subject.rowByValue(this.category.id); + assert.equal(row.value(), this.category.id); + assert.equal(this.category.parent_category_id, null); + }, + }); - componentTest("hideParentCategory (true)", { - template: template(["hideParentCategory=true"]), - - beforeEach() { - initCategoriesWithParentCategory(this); - }, - - async test(assert) { - await this.subject.expand(); - - const parentRow = this.subject.rowByValue(this.parentCategory.id); - assert.notOk(parentRow.exists(), "the parent row is not showing"); - - const childCategory = this.categories.firstObject; - const childCategoryId = childCategory.id; - const childRow = this.subject.rowByValue(childCategoryId); - assert.ok(childRow.exists(), "the child row is showing"); - - const $categoryStatus = childRow.el().find(".category-status"); - assert.ok($categoryStatus.text().trim().match(/^spec/)); - }, - }); - - componentTest("allow_uncategorized_topics (true)", { - template: template(), - - beforeEach() { - this.siteSettings.allow_uncategorized_topics = true; - initCategories(this); - }, - - async test(assert) { - await this.subject.expand(); - - const uncategorizedCategoryId = this.site.uncategorized_category_id; - const row = this.subject.rowByValue(uncategorizedCategoryId); - assert.ok(row.exists(), "the uncategorized row is showing"); - }, - }); - - componentTest("allow_uncategorized_topics (false)", { - template: template(), - - beforeEach() { - this.siteSettings.allow_uncategorized_topics = false; - initCategories(this); - }, - - async test(assert) { - await this.subject.expand(); - - const uncategorizedCategoryId = this.site.uncategorized_category_id; - const row = this.subject.rowByValue(uncategorizedCategoryId); - assert.notOk(row.exists(), "the uncategorized row is not showing"); - }, - }); - - componentTest("countSubcategories (default: false)", { - template: template(), - - beforeEach() { - initCategories(this); - }, - - async test(assert) { - await this.subject.expand(); - - const category = Category.findById(7); - const row = this.subject.rowByValue(category.id); - const topicCount = row.el().find(".topic-count").text().trim(); - - assert.equal( - topicCount, - "× 481", - "it doesn't include the topic count of subcategories" - ); - }, - }); - - componentTest("countSubcategories (true)", { - template: template(["countSubcategories=true"]), - - beforeEach() { - initCategories(this); - }, - - async test(assert) { - await this.subject.expand(); - - const category = Category.findById(7); - const row = this.subject.rowByValue(category.id); - const topicCount = row.el().find(".topic-count").text().trim(); - - assert.equal( - topicCount, - "× 584", - "it includes the topic count of subcategories" - ); - }, - }); - - componentTest("shortcuts:default", { - template: template(), - - beforeEach() { - initCategories(this); - this.set("category", null); - }, - - async test(assert) { - await this.subject.expand(); - - assert.equal( - this.subject.rowByIndex(0).value(), - this.categories.firstObject.id, - "Shortcuts are not prepended when no category is selected" - ); - }, - }); - - componentTest("shortcuts:category is set", { - template: template(), - - beforeEach() { - initCategories(this); - }, - - async test(assert) { - await this.subject.expand(); - - assert.equal(this.subject.rowByIndex(0).value(), ALL_CATEGORIES_ID); - }, - }); - - componentTest("shortcuts with parentCategory/subCategory=true:default", { - template: template(["subCategory=true"]), - - beforeEach() { - initCategoriesWithParentCategory(this); - }, - - async test(assert) { - await this.subject.expand(); - - assert.equal(this.subject.rowByIndex(0).value(), NO_CATEGORIES_ID); - }, - }); - - componentTest( - "shortcuts with parentCategory/subCategory=true:category is selected", - { - template: template(["subCategory=true"]), + componentTest("hideParentCategory (true)", { + template: template(["hideParentCategory=true"]), beforeEach() { initCategoriesWithParentCategory(this); - this.set("category", this.categories.firstObject); + }, + + async test(assert) { + await this.subject.expand(); + + const parentRow = this.subject.rowByValue(this.parentCategory.id); + assert.notOk(parentRow.exists(), "the parent row is not showing"); + + const childCategory = this.categories.firstObject; + const childCategoryId = childCategory.id; + const childRow = this.subject.rowByValue(childCategoryId); + assert.ok(childRow.exists(), "the child row is showing"); + + const $categoryStatus = childRow.el().find(".category-status"); + assert.ok($categoryStatus.text().trim().match(/^spec/)); + }, + }); + + componentTest("allow_uncategorized_topics (true)", { + template: template(), + + beforeEach() { + this.siteSettings.allow_uncategorized_topics = true; + initCategories(this); + }, + + async test(assert) { + await this.subject.expand(); + + const uncategorizedCategoryId = this.site.uncategorized_category_id; + const row = this.subject.rowByValue(uncategorizedCategoryId); + assert.ok(row.exists(), "the uncategorized row is showing"); + }, + }); + + componentTest("allow_uncategorized_topics (false)", { + template: template(), + + beforeEach() { + this.siteSettings.allow_uncategorized_topics = false; + initCategories(this); + }, + + async test(assert) { + await this.subject.expand(); + + const uncategorizedCategoryId = this.site.uncategorized_category_id; + const row = this.subject.rowByValue(uncategorizedCategoryId); + assert.notOk(row.exists(), "the uncategorized row is not showing"); + }, + }); + + componentTest("countSubcategories (default: false)", { + template: template(), + + beforeEach() { + initCategories(this); + }, + + async test(assert) { + await this.subject.expand(); + + const category = Category.findById(7); + const row = this.subject.rowByValue(category.id); + const topicCount = row.el().find(".topic-count").text().trim(); + + assert.equal( + topicCount, + "× 481", + "it doesn't include the topic count of subcategories" + ); + }, + }); + + componentTest("countSubcategories (true)", { + template: template(["countSubcategories=true"]), + + beforeEach() { + initCategories(this); + }, + + async test(assert) { + await this.subject.expand(); + + const category = Category.findById(7); + const row = this.subject.rowByValue(category.id); + const topicCount = row.el().find(".topic-count").text().trim(); + + assert.equal( + topicCount, + "× 584", + "it includes the topic count of subcategories" + ); + }, + }); + + componentTest("shortcuts:default", { + template: template(), + + beforeEach() { + initCategories(this); + this.set("category", null); + }, + + async test(assert) { + await this.subject.expand(); + + assert.equal( + this.subject.rowByIndex(0).value(), + this.categories.firstObject.id, + "Shortcuts are not prepended when no category is selected" + ); + }, + }); + + componentTest("shortcuts:category is set", { + template: template(), + + beforeEach() { + initCategories(this); }, async test(assert) { await this.subject.expand(); assert.equal(this.subject.rowByIndex(0).value(), ALL_CATEGORIES_ID); - assert.equal(this.subject.rowByIndex(1).value(), NO_CATEGORIES_ID); }, - } - ); + }); - componentTest("category url", { - template: template(), + componentTest("shortcuts with parentCategory/subCategory=true:default", { + template: template(["subCategory=true"]), - beforeEach() { - initCategoriesWithParentCategory(this); - sinon.stub(DiscourseURL, "routeTo"); - }, + beforeEach() { + initCategoriesWithParentCategory(this); + }, - async test(assert) { - await this.subject.expand(); - await this.subject.selectRowByValue(26); + async test(assert) { + await this.subject.expand(); - assert.ok( - DiscourseURL.routeTo.calledWith("/c/feature/spec/26"), - "it builds a correct URL" - ); - }, - }); -}); + assert.equal(this.subject.rowByIndex(0).value(), NO_CATEGORIES_ID); + }, + }); + + componentTest( + "shortcuts with parentCategory/subCategory=true:category is selected", + { + template: template(["subCategory=true"]), + + beforeEach() { + initCategoriesWithParentCategory(this); + this.set("category", this.categories.firstObject); + }, + + async test(assert) { + await this.subject.expand(); + + assert.equal(this.subject.rowByIndex(0).value(), ALL_CATEGORIES_ID); + assert.equal(this.subject.rowByIndex(1).value(), NO_CATEGORIES_ID); + }, + } + ); + + componentTest("category url", { + template: template(), + + beforeEach() { + initCategoriesWithParentCategory(this); + sinon.stub(DiscourseURL, "routeTo"); + }, + + async test(assert) { + await this.subject.expand(); + await this.subject.selectRowByValue(26); + + assert.ok( + DiscourseURL.routeTo.calledWith("/c/feature/spec/26"), + "it builds a correct URL" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/combo-box-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/combo-box-test.js index fbd9e4cc03..67176a1818 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/combo-box-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/combo-box-test.js @@ -24,17 +24,17 @@ const setDefaultState = (ctx, options) => { ctx.setProperties(properties); }; -discourseModule("Integration | Component | select-kit/combo-box", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | select-kit/combo-box", + function (hooks) { + setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set("subject", selectKit()); - }); + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); - componentTest("options.clearable", { - template: ` + componentTest("options.clearable", { + template: ` {{combo-box value=value content=content @@ -43,36 +43,36 @@ discourseModule("Integration | Component | select-kit/combo-box", function ( }} `, - beforeEach() { - setDefaultState(this, { - clearable: true, - onChange: (value) => { - this.set("value", value); - }, - }); - }, + beforeEach() { + setDefaultState(this, { + clearable: true, + onChange: (value) => { + this.set("value", value); + }, + }); + }, - async test(assert) { - const $header = this.subject.header(); + async test(assert) { + const $header = this.subject.header(); - assert.ok( - exists($header.el().find(".btn-clear")), - "it shows the clear button" - ); - assert.equal($header.value(), DEFAULT_VALUE); + assert.ok( + exists($header.el().find(".btn-clear")), + "it shows the clear button" + ); + assert.equal($header.value(), DEFAULT_VALUE); - await click($header.el().find(".btn-clear")); + await click($header.el().find(".btn-clear")); - assert.notOk( - exists($header.el().find(".btn-clear")), - "it hides the clear button" - ); - assert.equal($header.value(), null); - }, - }); + assert.notOk( + exists($header.el().find(".btn-clear")), + "it hides the clear button" + ); + assert.equal($header.value(), null); + }, + }); - componentTest("options.{caretUpIcon,caretDownIcon}", { - template: ` + componentTest("options.{caretUpIcon,caretDownIcon}", { + template: ` {{combo-box value=value content=content @@ -83,27 +83,28 @@ discourseModule("Integration | Component | select-kit/combo-box", function ( }} `, - beforeEach() { - setDefaultState(this, { - caretUpIcon: "pencil-alt", - caretDownIcon: "trash-alt", - }); - }, + beforeEach() { + setDefaultState(this, { + caretUpIcon: "pencil-alt", + caretDownIcon: "trash-alt", + }); + }, - async test(assert) { - const $header = this.subject.header().el(); + async test(assert) { + const $header = this.subject.header().el(); - assert.ok( - exists($header.find(`.d-icon-${this.caretDownIcon}`)), - "it uses the icon provided" - ); + assert.ok( + exists($header.find(`.d-icon-${this.caretDownIcon}`)), + "it uses the icon provided" + ); - await this.subject.expand(); + await this.subject.expand(); - assert.ok( - exists($header.find(`.d-icon-${this.caretUpIcon}`)), - "it uses the icon provided" - ); - }, - }); -}); + assert.ok( + exists($header.find(`.d-icon-${this.caretUpIcon}`)), + "it uses the icon provided" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/list-setting-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/list-setting-test.js index d7dfbe6a3f..0999e83e38 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/list-setting-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/list-setting-test.js @@ -16,31 +16,32 @@ function template(options = []) { `; } -discourseModule("Integration | Component | select-kit/list-setting", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | select-kit/list-setting", + function (hooks) { + setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set("subject", selectKit()); - }); + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); - componentTest("default", { - template: template(), + componentTest("default", { + template: template(), - beforeEach() { - this.set("value", ["bold", "italic"]); - this.set("choices", ["bold", "italic", "underline"]); - }, + beforeEach() { + this.set("value", ["bold", "italic"]); + this.set("choices", ["bold", "italic", "underline"]); + }, - async test(assert) { - assert.equal(this.subject.header().name(), "bold,italic"); - assert.equal(this.subject.header().value(), "bold,italic"); + async test(assert) { + assert.equal(this.subject.header().name(), "bold,italic"); + assert.equal(this.subject.header().value(), "bold,italic"); - await this.subject.expand(); + await this.subject.expand(); - assert.equal(this.subject.rows().length, 1); - assert.equal(this.subject.rowByIndex(0).value(), "underline"); - }, - }); -}); + assert.equal(this.subject.rows().length, 1); + assert.equal(this.subject.rowByIndex(0).value(), "underline"); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/multi-select-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/multi-select-test.js index 503966d994..38c934338d 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/multi-select-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/multi-select-test.js @@ -33,42 +33,43 @@ const setDefaultState = (ctx, options) => { ctx.setProperties(properties); }; -discourseModule("Integration | Component | select-kit/multi-select", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | select-kit/multi-select", + function (hooks) { + setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set("subject", selectKit()); - }); + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); - componentTest("content", { - template: template(), + componentTest("content", { + template: template(), - beforeEach() { - setDefaultState(this); - }, + beforeEach() { + setDefaultState(this); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const content = this.subject.displayedContent(); - assert.equal(content.length, 3, "it shows rows"); - assert.equal( - content[0].name, - this.content.firstObject.name, - "it has the correct name" - ); - assert.equal( - content[0].id, - this.content.firstObject.id, - "it has the correct value" - ); - assert.equal( - this.subject.header().value(), - null, - "it doesn't set a value from the content" - ); - }, - }); -}); + const content = this.subject.displayedContent(); + assert.equal(content.length, 3, "it shows rows"); + assert.equal( + content[0].name, + this.content.firstObject.name, + "it has the correct name" + ); + assert.equal( + content[0].id, + this.content.firstObject.id, + "it has the correct value" + ); + assert.equal( + this.subject.header().value(), + null, + "it doesn't set a value from the content" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js index ac9c4ee1b6..8deb9d73d6 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js @@ -44,103 +44,106 @@ const setDefaultState = (ctx, options) => { ctx.setProperties(properties); }; -discourseModule("Integration | Component | select-kit/single-select", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | select-kit/single-select", + function (hooks) { + setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set("subject", selectKit()); - }); + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); - componentTest("content", { - template: "{{single-select content=content}}", + componentTest("content", { + template: "{{single-select content=content}}", - beforeEach() { - setDefaultState(this); - }, + beforeEach() { + setDefaultState(this); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const content = this.subject.displayedContent(); - assert.equal(content.length, 3, "it shows rows"); - assert.equal( - content[0].name, - this.content.firstObject.name, - "it has the correct name" - ); - assert.equal( - content[0].id, - this.content.firstObject.id, - "it has the correct value" - ); - assert.equal( - this.subject.header().value(), - null, - "it doesn't set a value from the content" - ); - }, - }); + const content = this.subject.displayedContent(); + assert.equal(content.length, 3, "it shows rows"); + assert.equal( + content[0].name, + this.content.firstObject.name, + "it has the correct name" + ); + assert.equal( + content[0].id, + this.content.firstObject.id, + "it has the correct value" + ); + assert.equal( + this.subject.header().value(), + null, + "it doesn't set a value from the content" + ); + }, + }); - componentTest("value", { - template: template(), + componentTest("value", { + template: template(), - beforeEach() { - setDefaultState(this); - }, + beforeEach() { + setDefaultState(this); + }, - test(assert) { - assert.equal( - this.subject.header().value(this.content), - 1, - "it selects the correct content to display" - ); - }, - }); + test(assert) { + assert.equal( + this.subject.header().value(this.content), + 1, + "it selects the correct content to display" + ); + }, + }); - componentTest("options.filterable", { - template: template(["filterable=filterable"]), + componentTest("options.filterable", { + template: template(["filterable=filterable"]), - beforeEach() { - setDefaultState(this, { filterable: true }); - }, + beforeEach() { + setDefaultState(this, { filterable: true }); + }, - async test(assert) { - await this.subject.expand(); - assert.ok(this.subject.filter().exists(), "it shows the filter"); + async test(assert) { + await this.subject.expand(); + assert.ok(this.subject.filter().exists(), "it shows the filter"); - const filter = this.subject.displayedContent()[1].name; - await this.subject.fillInFilter(filter); - assert.equal( - this.subject.displayedContent()[0].name, - filter, - "it filters the list" - ); - }, - }); + const filter = this.subject.displayedContent()[1].name; + await this.subject.fillInFilter(filter); + assert.equal( + this.subject.displayedContent()[0].name, + filter, + "it filters the list" + ); + }, + }); - componentTest("options.limitMatches", { - template: template(["limitMatches=limitMatches", "filterable=filterable"]), + componentTest("options.limitMatches", { + template: template([ + "limitMatches=limitMatches", + "filterable=filterable", + ]), - beforeEach() { - setDefaultState(this, { limitMatches: 1, filterable: true }); - }, + beforeEach() { + setDefaultState(this, { limitMatches: 1, filterable: true }); + }, - async test(assert) { - await this.subject.expand(); - await this.subject.fillInFilter("ba"); + async test(assert) { + await this.subject.expand(); + await this.subject.fillInFilter("ba"); - assert.equal( - this.subject.displayedContent().length, - 1, - "it returns only 1 result" - ); - }, - }); + assert.equal( + this.subject.displayedContent().length, + 1, + "it returns only 1 result" + ); + }, + }); - componentTest("valueAttribute (deprecated)", { - template: ` + componentTest("valueAttribute (deprecated)", { + template: ` {{single-select value=value content=content @@ -148,200 +151,201 @@ discourseModule("Integration | Component | select-kit/single-select", function ( }} `, - beforeEach() { - this.set("value", "normal"); + beforeEach() { + this.set("value", "normal"); - const content = [ - { name: "Smallest", value: "smallest" }, - { name: "Smaller", value: "smaller" }, - { name: "Normal", value: "normal" }, - { name: "Larger", value: "larger" }, - { name: "Largest", value: "largest" }, - ]; - this.set("content", content); - }, + const content = [ + { name: "Smallest", value: "smallest" }, + { name: "Smaller", value: "smaller" }, + { name: "Normal", value: "normal" }, + { name: "Larger", value: "larger" }, + { name: "Largest", value: "largest" }, + ]; + this.set("content", content); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - assert.equal(this.subject.selectedRow().value(), this.value); - }, - }); + assert.equal(this.subject.selectedRow().value(), this.value); + }, + }); - componentTest("none:string", { - template: template(['none="test.none"']), + componentTest("none:string", { + template: template(['none="test.none"']), - beforeEach() { - I18n.translations[I18n.locale].js.test = { none: "(default)" }; - setDefaultState(this, { value: 1 }); - }, + beforeEach() { + I18n.translations[I18n.locale].js.test = { none: "(default)" }; + setDefaultState(this, { value: 1 }); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const noneRow = this.subject.rowByIndex(0); - assert.equal(noneRow.value(), null); - assert.equal(noneRow.name(), I18n.t("test.none")); - }, - }); + const noneRow = this.subject.rowByIndex(0); + assert.equal(noneRow.value(), null); + assert.equal(noneRow.name(), I18n.t("test.none")); + }, + }); - componentTest("none:object", { - template: template(["none=none"]), + componentTest("none:object", { + template: template(["none=none"]), - beforeEach() { - setDefaultState(this, { none: { value: null, name: "(default)" } }); - }, + beforeEach() { + setDefaultState(this, { none: { value: null, name: "(default)" } }); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const noneRow = this.subject.rowByIndex(0); - assert.equal(noneRow.value(), null); - assert.equal(noneRow.name(), "(default)"); - }, - }); + const noneRow = this.subject.rowByIndex(0); + assert.equal(noneRow.value(), null); + assert.equal(noneRow.name(), "(default)"); + }, + }); - componentTest("content is a basic array", { - template: template(['none="test.none"']), + componentTest("content is a basic array", { + template: template(['none="test.none"']), - beforeEach() { - I18n.translations[I18n.locale].js.test = { none: "(default)" }; - setDefaultState(this, { - nameProperty: null, - valueProperty: null, - value: "foo", - content: ["foo", "bar", "baz"], - }); - }, + beforeEach() { + I18n.translations[I18n.locale].js.test = { none: "(default)" }; + setDefaultState(this, { + nameProperty: null, + valueProperty: null, + value: "foo", + content: ["foo", "bar", "baz"], + }); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - const noneRow = this.subject.rowByIndex(0); - assert.equal(noneRow.value(), I18n.t("test.none")); - assert.equal(noneRow.name(), I18n.t("test.none")); - assert.equal(this.value, "foo"); + const noneRow = this.subject.rowByIndex(0); + assert.equal(noneRow.value(), I18n.t("test.none")); + assert.equal(noneRow.name(), I18n.t("test.none")); + assert.equal(this.value, "foo"); - await this.subject.selectRowByIndex(0); + await this.subject.selectRowByIndex(0); - assert.equal(this.value, null); - }, - }); + assert.equal(this.value, null); + }, + }); - componentTest("selected value can be 0", { - template: template(), + componentTest("selected value can be 0", { + template: template(), - beforeEach() { - setDefaultState(this, { - value: 1, - content: [ - { id: 0, name: "foo" }, - { id: 1, name: "bar" }, - ], - }); - }, + beforeEach() { + setDefaultState(this, { + value: 1, + content: [ + { id: 0, name: "foo" }, + { id: 1, name: "bar" }, + ], + }); + }, - async test(assert) { - assert.equal(this.subject.header().value(), 1); + async test(assert) { + assert.equal(this.subject.header().value(), 1); - await this.subject.expand(); - await this.subject.selectRowByValue(0); + await this.subject.expand(); + await this.subject.selectRowByValue(0); - assert.equal(this.subject.header().value(), 0); - }, - }); + assert.equal(this.subject.header().value(), 0); + }, + }); - componentTest("prevents propagating click event on header", { - template: - "{{#d-button icon='times' action=onClick}}{{single-select options=(hash preventsClickPropagation=true) value=value content=content}}{{/d-button}}", + componentTest("prevents propagating click event on header", { + template: + "{{#d-button icon='times' action=onClick}}{{single-select options=(hash preventsClickPropagation=true) value=value content=content}}{{/d-button}}", - beforeEach() { - this.setProperties({ - onClick: () => this.set("value", "foo"), - content: DEFAULT_CONTENT, - value: DEFAULT_VALUE, - }); - }, + beforeEach() { + this.setProperties({ + onClick: () => this.set("value", "foo"), + content: DEFAULT_CONTENT, + value: DEFAULT_VALUE, + }); + }, - async test(assert) { - assert.equal(this.value, DEFAULT_VALUE); - await this.subject.expand(); - assert.equal(this.value, DEFAULT_VALUE); - }, - }); + async test(assert) { + assert.equal(this.value, DEFAULT_VALUE); + await this.subject.expand(); + assert.equal(this.value, DEFAULT_VALUE); + }, + }); - componentTest("labelProperty", { - template: - '{{single-select labelProperty="foo" value=value content=content}}', + componentTest("labelProperty", { + template: + '{{single-select labelProperty="foo" value=value content=content}}', - beforeEach() { - this.setProperties({ - content: [{ id: 1, name: "john", foo: "JACKSON" }], - value: 1, - }); - }, + beforeEach() { + this.setProperties({ + content: [{ id: 1, name: "john", foo: "JACKSON" }], + value: 1, + }); + }, - async test(assert) { - assert.equal(this.subject.header().label(), "JACKSON"); + async test(assert) { + assert.equal(this.subject.header().label(), "JACKSON"); - await this.subject.expand(); + await this.subject.expand(); - const row = this.subject.rowByValue(1); + const row = this.subject.rowByValue(1); - assert.equal(row.label(), "JACKSON"); - }, - }); + assert.equal(row.label(), "JACKSON"); + }, + }); - componentTest("titleProperty", { - template: - '{{single-select titleProperty="foo" value=value content=content}}', + componentTest("titleProperty", { + template: + '{{single-select titleProperty="foo" value=value content=content}}', - beforeEach() { - this.setProperties({ - content: [{ id: 1, name: "john", foo: "JACKSON" }], - value: 1, - }); - }, + beforeEach() { + this.setProperties({ + content: [{ id: 1, name: "john", foo: "JACKSON" }], + value: 1, + }); + }, - async test(assert) { - assert.equal(this.subject.header().title(), "JACKSON"); + async test(assert) { + assert.equal(this.subject.header().title(), "JACKSON"); - await this.subject.expand(); + await this.subject.expand(); - const row = this.subject.rowByValue(1); + const row = this.subject.rowByValue(1); - assert.equal(row.title(), "JACKSON"); - }, - }); + assert.equal(row.title(), "JACKSON"); + }, + }); - componentTest("langProperty", { - template: - '{{single-select langProperty="foo" value=value content=content}}', + componentTest("langProperty", { + template: + '{{single-select langProperty="foo" value=value content=content}}', - beforeEach() { - this.setProperties({ - content: [{ id: 1, name: "john", foo: "be" }], - value: null, - }); - }, + beforeEach() { + this.setProperties({ + content: [{ id: 1, name: "john", foo: "be" }], + value: null, + }); + }, - async test(assert) { - assert.equal( - this.subject.header().el()[0].querySelector(".selected-name").lang, - "" - ); + async test(assert) { + assert.equal( + this.subject.header().el()[0].querySelector(".selected-name").lang, + "" + ); - await this.subject.expand(); + await this.subject.expand(); - const row = this.subject.rowByValue(1); - assert.equal(row.el()[0].lang, "be"); + const row = this.subject.rowByValue(1); + assert.equal(row.el()[0].lang, "be"); - await this.subject.selectRowByValue(1); + await this.subject.selectRowByValue(1); - assert.equal( - this.subject.header().el()[0].querySelector(".selected-name").lang, - "be" - ); - }, - }); -}); + assert.equal( + this.subject.header().el()[0].querySelector(".selected-name").lang, + "be" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js index 55df881fdf..5b6ca63707 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js @@ -36,63 +36,64 @@ function template(options = []) { `; } -discourseModule("Integration | Component | select-kit/tag-drop", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | select-kit/tag-drop", + function (hooks) { + setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set("subject", selectKit()); + hooks.beforeEach(function () { + this.set("subject", selectKit()); - const site = Site.current(); - set(site, "top_tags", ["jeff", "neil", "arpit", "régis"]); + const site = Site.current(); + set(site, "top_tags", ["jeff", "neil", "arpit", "régis"]); - const response = (object) => { - return [200, { "Content-Type": "application/json" }, object]; - }; + const response = (object) => { + return [200, { "Content-Type": "application/json" }, object]; + }; - pretender.get("/tags/filter/search", (params) => { - if (params.queryParams.q === "rég") { - return response({ - results: [{ id: "régis", text: "régis", count: 2, pm_count: 0 }], - }); - } else if (params.queryParams.q === "dav") { - return response({ - results: [{ id: "David", text: "David", count: 2, pm_count: 0 }], - }); - } + pretender.get("/tags/filter/search", (params) => { + if (params.queryParams.q === "rég") { + return response({ + results: [{ id: "régis", text: "régis", count: 2, pm_count: 0 }], + }); + } else if (params.queryParams.q === "dav") { + return response({ + results: [{ id: "David", text: "David", count: 2, pm_count: 0 }], + }); + } + }); }); - }); - componentTest("default", { - template: template(["tagId=tagId"]), + componentTest("default", { + template: template(["tagId=tagId"]), - beforeEach() { - initTags(this); - }, + beforeEach() { + initTags(this); + }, - async test(assert) { - await this.subject.expand(); + async test(assert) { + await this.subject.expand(); - assert.ok(true); - // const row = this.subject.rowByValue(this.category.id); - // assert.ok( - // exists(row.el().find(".category-desc")), - // "it shows category description for newcomers" - // ); + assert.ok(true); + // const row = this.subject.rowByValue(this.category.id); + // assert.ok( + // exists(row.el().find(".category-desc")), + // "it shows category description for newcomers" + // ); - const content = this.subject.displayedContent(); + const content = this.subject.displayedContent(); - assert.equal( - content[0].name, - I18n.t("tagging.selector_no_tags"), - "it has the translated label for no-tags" - ); - assert.equal( - content[1].name, - I18n.t("tagging.selector_all_tags"), - "it has the correct label for all-tags" - ); - }, - }); -}); + assert.equal( + content[0].name, + I18n.t("tagging.selector_no_tags"), + "it has the translated label for no-tags" + ); + assert.equal( + content[1].name, + I18n.t("tagging.selector_all_tags"), + "it has the correct label for all-tags" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js index 8eae6bf75b..7066ee74f1 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js @@ -4,37 +4,38 @@ import componentTest, { import { discourseModule } from "discourse/tests/helpers/qunit-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; -discourseModule("Integration | Component | select-kit/user-chooser", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | select-kit/user-chooser", + function (hooks) { + setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set("subject", selectKit()); - }); + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); - componentTest("displays usernames", { - template: `{{user-chooser value=value}}`, + componentTest("displays usernames", { + template: `{{user-chooser value=value}}`, - beforeEach() { - this.set("value", ["bob", "martin"]); - }, + beforeEach() { + this.set("value", ["bob", "martin"]); + }, - async test(assert) { - assert.equal(this.subject.header().name(), "bob,martin"); - }, - }); + async test(assert) { + assert.equal(this.subject.header().name(), "bob,martin"); + }, + }); - componentTest("can remove a username", { - template: `{{user-chooser value=value}}`, + componentTest("can remove a username", { + template: `{{user-chooser value=value}}`, - beforeEach() { - this.set("value", ["bob", "martin"]); - }, + beforeEach() { + this.set("value", ["bob", "martin"]); + }, - async test(assert) { - await this.subject.deselectItem("bob"); - assert.equal(this.subject.header().name(), "martin"); - }, - }); -}); + async test(assert) { + await this.subject.deselectItem("bob"); + assert.equal(this.subject.header().name(), "martin"); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/actions-summary-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/actions-summary-test.js index 794c95338b..9afee06d23 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/actions-summary-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/actions-summary-test.js @@ -6,29 +6,30 @@ import { queryAll, } from "discourse/tests/helpers/qunit-helpers"; -discourseModule("Integration | Component | Widget | actions-summary", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | actions-summary", + function (hooks) { + setupRenderingTest(hooks); - componentTest("post deleted", { - template: '{{mount-widget widget="actions-summary" args=args}}', - beforeEach() { - this.set("args", { - deleted_at: "2016-01-01", - deletedByUsername: "eviltrout", - deletedByAvatarTemplate: "/images/avatar.png", - }); - }, - test(assert) { - assert.ok( - queryAll(".post-action .d-icon-far-trash-alt").length === 1, - "it has the deleted icon" - ); - assert.ok( - queryAll(".avatar[title=eviltrout]").length === 1, - "it has the deleted by avatar" - ); - }, - }); -}); + componentTest("post deleted", { + template: '{{mount-widget widget="actions-summary" args=args}}', + beforeEach() { + this.set("args", { + deleted_at: "2016-01-01", + deletedByUsername: "eviltrout", + deletedByAvatarTemplate: "/images/avatar.png", + }); + }, + test(assert) { + assert.ok( + queryAll(".post-action .d-icon-far-trash-alt").length === 1, + "it has the deleted icon" + ); + assert.ok( + queryAll(".avatar[title=eviltrout]").length === 1, + "it has the deleted by avatar" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/avatar-flair-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/avatar-flair-test.js index 49a11aa97d..3267c81113 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/avatar-flair-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/avatar-flair-test.js @@ -6,41 +6,42 @@ import { queryAll, } from "discourse/tests/helpers/qunit-helpers"; -discourseModule("Integration | Component | Widget | avatar-flair", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | avatar-flair", + function (hooks) { + setupRenderingTest(hooks); - componentTest("avatar flair with an icon", { - template: '{{mount-widget widget="avatar-flair" args=args}}', - beforeEach() { - this.set("args", { - primary_group_flair_url: "fa-bars", - primary_group_flair_bg_color: "CC0000", - primary_group_flair_color: "FFFFFF", - }); - }, - test(assert) { - assert.ok(queryAll(".avatar-flair").length, "it has the tag"); - assert.ok(queryAll("svg.d-icon-bars").length, "it has the svg icon"); - assert.equal( - queryAll(".avatar-flair").attr("style"), - "background-color: #CC0000; color: #FFFFFF; ", - "it has styles" - ); - }, - }); + componentTest("avatar flair with an icon", { + template: '{{mount-widget widget="avatar-flair" args=args}}', + beforeEach() { + this.set("args", { + primary_group_flair_url: "fa-bars", + primary_group_flair_bg_color: "CC0000", + primary_group_flair_color: "FFFFFF", + }); + }, + test(assert) { + assert.ok(queryAll(".avatar-flair").length, "it has the tag"); + assert.ok(queryAll("svg.d-icon-bars").length, "it has the svg icon"); + assert.equal( + queryAll(".avatar-flair").attr("style"), + "background-color: #CC0000; color: #FFFFFF; ", + "it has styles" + ); + }, + }); - componentTest("avatar flair with an image", { - template: '{{mount-widget widget="avatar-flair" args=args}}', - beforeEach() { - this.set("args", { - primary_group_flair_url: "/images/avatar.png", - }); - }, - test(assert) { - assert.ok(queryAll(".avatar-flair").length, "it has the tag"); - assert.ok(queryAll("svg").length === 0, "it does not have an svg icon"); - }, - }); -}); + componentTest("avatar flair with an image", { + template: '{{mount-widget widget="avatar-flair" args=args}}', + beforeEach() { + this.set("args", { + primary_group_flair_url: "/images/avatar.png", + }); + }, + test(assert) { + assert.ok(queryAll(".avatar-flair").length, "it has the tag"); + assert.ok(queryAll("svg").length === 0, "it does not have an svg icon"); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js index 343aeb729e..4f73a310db 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js @@ -3,6 +3,7 @@ import componentTest, { } from "discourse/tests/helpers/component-test"; import { discourseModule, + query, queryAll, } from "discourse/tests/helpers/qunit-helpers"; @@ -69,4 +70,29 @@ discourseModule("Integration | Component | Widget | button", function (hooks) { ); }, }); + + componentTest("translatedLabel", { + template: '{{mount-widget widget="button" args=args}}', + + beforeEach() { + this.set("args", { translatedLabel: "foo bar" }); + }, + + test(assert) { + assert.equal(query("button span.d-button-label").innerText, "foo bar"); + }, + }); + + componentTest("translatedTitle", { + template: '{{mount-widget widget="button" args=args}}', + + beforeEach() { + this.set("args", { label: "topic.create", translatedTitle: "foo bar" }); + }, + + test(assert) { + assert.equal(query("button").title, "foo bar"); + assert.equal(query("button").ariaLabel, "foo bar"); + }, + }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/hamburger-menu-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/hamburger-menu-test.js index 43638125ea..8eb98821e8 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/hamburger-menu-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/hamburger-menu-test.js @@ -12,252 +12,256 @@ let mutedCategoryIds = []; let unreadCategoryIds = []; let categoriesByCount = []; -discourseModule("Integration | Component | Widget | hamburger-menu", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | hamburger-menu", + function (hooks) { + setupRenderingTest(hooks); - componentTest("prioritize faq", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("prioritize faq", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.siteSettings.faq_url = "http://example.com/faq"; - this.currentUser.set("read_faq", false); - }, + beforeEach() { + this.siteSettings.faq_url = "http://example.com/faq"; + this.currentUser.set("read_faq", false); + }, - test(assert) { - assert.ok(queryAll(".faq-priority").length); - assert.ok(!queryAll(".faq-link").length); - }, - }); + test(assert) { + assert.ok(queryAll(".faq-priority").length); + assert.ok(!queryAll(".faq-link").length); + }, + }); - componentTest("prioritize faq - user has read", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("prioritize faq - user has read", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.siteSettings.faq_url = "http://example.com/faq"; - this.currentUser.set("read_faq", true); - }, + beforeEach() { + this.siteSettings.faq_url = "http://example.com/faq"; + this.currentUser.set("read_faq", true); + }, - test(assert) { - assert.ok(!queryAll(".faq-priority").length); - assert.ok(queryAll(".faq-link").length); - }, - }); + test(assert) { + assert.ok(!queryAll(".faq-priority").length); + assert.ok(queryAll(".faq-link").length); + }, + }); - componentTest("staff menu - not staff", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("staff menu - not staff", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.currentUser.set("staff", false); - }, + beforeEach() { + this.currentUser.set("staff", false); + }, - test(assert) { - assert.ok(!queryAll(".admin-link").length); - }, - }); + test(assert) { + assert.ok(!queryAll(".admin-link").length); + }, + }); - componentTest("staff menu - moderator", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("staff menu - moderator", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.currentUser.set("moderator", true); - }, + beforeEach() { + this.currentUser.set("moderator", true); + }, - test(assert) { - assert.ok(queryAll(".admin-link").length); - assert.ok(queryAll(".review").length); - assert.ok(!queryAll(".settings-link").length); - }, - }); + test(assert) { + assert.ok(queryAll(".admin-link").length); + assert.ok(queryAll(".review").length); + assert.ok(!queryAll(".settings-link").length); + }, + }); - componentTest("staff menu - admin", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("staff menu - admin", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.currentUser.setProperties({ admin: true }); - }, + beforeEach() { + this.currentUser.setProperties({ admin: true }); + }, - test(assert) { - assert.ok(queryAll(".settings-link").length); - }, - }); + test(assert) { + assert.ok(queryAll(".settings-link").length); + }, + }); - componentTest("logged in links", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("logged in links", { + template: '{{mount-widget widget="hamburger-menu"}}', - test(assert) { - assert.ok(queryAll(".new-topics-link").length); - assert.ok(queryAll(".unread-topics-link").length); - }, - }); + test(assert) { + assert.ok(queryAll(".new-topics-link").length); + assert.ok(queryAll(".unread-topics-link").length); + }, + }); - componentTest("general links", { - template: '{{mount-widget widget="hamburger-menu"}}', - anonymous: true, + componentTest("general links", { + template: '{{mount-widget widget="hamburger-menu"}}', + anonymous: true, - test(assert) { - assert.ok(queryAll("li[class='']").length === 0); - assert.ok(queryAll(".latest-topics-link").length); - assert.ok(!queryAll(".new-topics-link").length); - assert.ok(!queryAll(".unread-topics-link").length); - assert.ok(queryAll(".top-topics-link").length); - assert.ok(queryAll(".badge-link").length); - assert.ok(queryAll(".category-link").length > 0); - }, - }); + test(assert) { + assert.ok(queryAll("li[class='']").length === 0); + assert.ok(queryAll(".latest-topics-link").length); + assert.ok(!queryAll(".new-topics-link").length); + assert.ok(!queryAll(".unread-topics-link").length); + assert.ok(queryAll(".top-topics-link").length); + assert.ok(queryAll(".badge-link").length); + assert.ok(queryAll(".category-link").length > 0); + }, + }); - let maxCategoriesToDisplay; + let maxCategoriesToDisplay; - componentTest("top categories - anonymous", { - template: '{{mount-widget widget="hamburger-menu"}}', - anonymous: true, + componentTest("top categories - anonymous", { + template: '{{mount-widget widget="hamburger-menu"}}', + anonymous: true, - beforeEach() { - this.siteSettings.header_dropdown_category_count = 8; - }, + beforeEach() { + this.siteSettings.header_dropdown_category_count = 8; + }, - test(assert) { - assert.equal(queryAll(".category-link").length, 8); - assert.equal( - queryAll(".category-link .category-name").text(), - this.site - .get("categoriesByCount") - .slice(0, 8) - .map((c) => c.name) - .join("") - ); - }, - }); + test(assert) { + assert.equal(queryAll(".category-link").length, 8); + assert.equal( + queryAll(".category-link .category-name").text(), + this.site + .get("categoriesByCount") + .slice(0, 8) + .map((c) => c.name) + .join("") + ); + }, + }); - componentTest("top categories - allow_uncategorized_topics", { - template: '{{mount-widget widget="hamburger-menu"}}', - anonymous: true, + componentTest("top categories - allow_uncategorized_topics", { + template: '{{mount-widget widget="hamburger-menu"}}', + anonymous: true, - beforeEach() { - this.siteSettings.allow_uncategorized_topics = false; - this.siteSettings.header_dropdown_category_count = 8; - }, + beforeEach() { + this.siteSettings.allow_uncategorized_topics = false; + this.siteSettings.header_dropdown_category_count = 8; + }, - test(assert) { - assert.equal(queryAll(".category-link").length, 8); - assert.equal( - queryAll(".category-link .category-name").text(), - this.site - .get("categoriesByCount") - .filter((c) => c.name !== "uncategorized") - .slice(0, 8) - .map((c) => c.name) - .join("") - ); - }, - }); + test(assert) { + assert.equal(queryAll(".category-link").length, 8); + assert.equal( + queryAll(".category-link .category-name").text(), + this.site + .get("categoriesByCount") + .filter((c) => c.name !== "uncategorized") + .slice(0, 8) + .map((c) => c.name) + .join("") + ); + }, + }); - componentTest("top categories", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("top categories", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.siteSettings.header_dropdown_category_count = 8; - maxCategoriesToDisplay = this.siteSettings.header_dropdown_category_count; - categoriesByCount = this.site.get("categoriesByCount").slice(); - categoriesByCount.every((c) => { - if (!topCategoryIds.includes(c.id)) { - if (mutedCategoryIds.length === 0) { - mutedCategoryIds.push(c.id); - c.set("notification_level", NotificationLevels.MUTED); - } else if (unreadCategoryIds.length === 0) { - unreadCategoryIds.push(c.id); - for (let i = 0; i < 5; i++) { - c.topicTrackingState.states["t123" + i] = { - category_id: c.id, - last_read_post_number: 1, - highest_post_number: 2, - notification_level: NotificationLevels.TRACKING, - }; + beforeEach() { + this.siteSettings.header_dropdown_category_count = 8; + maxCategoriesToDisplay = this.siteSettings + .header_dropdown_category_count; + categoriesByCount = this.site.get("categoriesByCount").slice(); + categoriesByCount.every((c) => { + if (!topCategoryIds.includes(c.id)) { + if (mutedCategoryIds.length === 0) { + mutedCategoryIds.push(c.id); + c.set("notification_level", NotificationLevels.MUTED); + } else if (unreadCategoryIds.length === 0) { + unreadCategoryIds.push(c.id); + for (let i = 0; i < 5; i++) { + c.topicTrackingState.states["t123" + i] = { + category_id: c.id, + last_read_post_number: 1, + highest_post_number: 2, + notification_level: NotificationLevels.TRACKING, + }; + } + } else { + unreadCategoryIds.splice(0, 0, c.id); + for (let i = 0; i < 10; i++) { + c.topicTrackingState.states["t321" + i] = { + category_id: c.id, + last_read_post_number: null, + }; + } + return false; } - } else { - unreadCategoryIds.splice(0, 0, c.id); - for (let i = 0; i < 10; i++) { - c.topicTrackingState.states["t321" + i] = { - category_id: c.id, - last_read_post_number: null, - }; - } - return false; } - } - return true; - }); - this.currentUser.set("top_category_ids", topCategoryIds); - }, + return true; + }); + this.currentUser.set("top_category_ids", topCategoryIds); + }, - test(assert) { - assert.equal(queryAll(".category-link").length, maxCategoriesToDisplay); + test(assert) { + assert.equal(queryAll(".category-link").length, maxCategoriesToDisplay); - categoriesByCount = categoriesByCount.filter( - (c) => !mutedCategoryIds.includes(c.id) - ); - let ids = [ - ...unreadCategoryIds, - ...topCategoryIds, - ...categoriesByCount.map((c) => c.id), - ] - .uniq() - .slice(0, maxCategoriesToDisplay); + categoriesByCount = categoriesByCount.filter( + (c) => !mutedCategoryIds.includes(c.id) + ); + let ids = [ + ...unreadCategoryIds, + ...topCategoryIds, + ...categoriesByCount.map((c) => c.id), + ] + .uniq() + .slice(0, maxCategoriesToDisplay); - assert.equal( - queryAll(".category-link .category-name").text(), - ids.map((i) => categoriesByCount.find((c) => c.id === i).name).join("") - ); - }, - }); + assert.equal( + queryAll(".category-link .category-name").text(), + ids + .map((i) => categoriesByCount.find((c) => c.id === i).name) + .join("") + ); + }, + }); - componentTest("badges link - disabled", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("badges link - disabled", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.siteSettings.enable_badges = false; - }, + beforeEach() { + this.siteSettings.enable_badges = false; + }, - test(assert) { - assert.ok(!queryAll(".badge-link").length); - }, - }); + test(assert) { + assert.ok(!queryAll(".badge-link").length); + }, + }); - componentTest("badges link", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("badges link", { + template: '{{mount-widget widget="hamburger-menu"}}', - test(assert) { - assert.ok(queryAll(".badge-link").length); - }, - }); + test(assert) { + assert.ok(queryAll(".badge-link").length); + }, + }); - componentTest("user directory link", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("user directory link", { + template: '{{mount-widget widget="hamburger-menu"}}', - test(assert) { - assert.ok(queryAll(".user-directory-link").length); - }, - }); + test(assert) { + assert.ok(queryAll(".user-directory-link").length); + }, + }); - componentTest("user directory link - disabled", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("user directory link - disabled", { + template: '{{mount-widget widget="hamburger-menu"}}', - beforeEach() { - this.siteSettings.enable_user_directory = false; - }, + beforeEach() { + this.siteSettings.enable_user_directory = false; + }, - test(assert) { - assert.ok(!queryAll(".user-directory-link").length); - }, - }); + test(assert) { + assert.ok(!queryAll(".user-directory-link").length); + }, + }); - componentTest("general links", { - template: '{{mount-widget widget="hamburger-menu"}}', + componentTest("general links", { + template: '{{mount-widget widget="hamburger-menu"}}', - test(assert) { - assert.ok(queryAll(".about-link").length); - assert.ok(queryAll(".keyboard-shortcuts-link").length); - }, - }); -}); + test(assert) { + assert.ok(queryAll(".about-link").length); + assert.ok(queryAll(".keyboard-shortcuts-link").length); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js index bf5d3d3684..c9d2932549 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js @@ -14,240 +14,241 @@ const darkLogo = "/images/d-logo-sketch.png?dark"; const title = "Cool Forum"; const prefersDark = "(prefers-color-scheme: dark)"; -discourseModule("Integration | Component | Widget | home-logo", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | home-logo", + function (hooks) { + setupRenderingTest(hooks); - componentTest("basics", { - template: '{{mount-widget widget="home-logo" args=args}}', - skip: true, - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_logo_small_url = smallLogo; - this.siteSettings.title = title; - this.set("args", { minimized: false }); - }, + componentTest("basics", { + template: '{{mount-widget widget="home-logo" args=args}}', + skip: true, + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_small_url = smallLogo; + this.siteSettings.title = title; + this.set("args", { minimized: false }); + }, - test(assert) { - assert.ok(queryAll(".title").length === 1); + test(assert) { + assert.ok(queryAll(".title").length === 1); - assert.ok(queryAll("img#site-logo.logo-big").length === 1); - assert.equal(queryAll("#site-logo").attr("src"), bigLogo); - assert.equal(queryAll("#site-logo").attr("alt"), title); - }, - }); + assert.ok(queryAll("img#site-logo.logo-big").length === 1); + assert.equal(queryAll("#site-logo").attr("src"), bigLogo); + assert.equal(queryAll("#site-logo").attr("alt"), title); + }, + }); - componentTest("basics - minimized", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_logo_small_url = smallLogo; - this.siteSettings.title = title; - this.set("args", { minimized: true }); - }, + componentTest("basics - minimized", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_small_url = smallLogo; + this.siteSettings.title = title; + this.set("args", { minimized: true }); + }, - test(assert) { - assert.ok(queryAll("img.logo-small").length === 1); - assert.equal(queryAll("img.logo-small").attr("src"), smallLogo); - assert.equal(queryAll("img.logo-small").attr("alt"), title); - assert.equal(queryAll("img.logo-small").attr("width"), 36); - }, - }); + test(assert) { + assert.ok(queryAll("img.logo-small").length === 1); + assert.equal(queryAll("img.logo-small").attr("src"), smallLogo); + assert.equal(queryAll("img.logo-small").attr("alt"), title); + assert.equal(queryAll("img.logo-small").attr("width"), 36); + }, + }); - componentTest("no logo", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = ""; - this.siteSettings.site_logo_small_url = ""; - this.siteSettings.title = title; - this.set("args", { minimized: false }); - }, + componentTest("no logo", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = ""; + this.siteSettings.site_logo_small_url = ""; + this.siteSettings.title = title; + this.set("args", { minimized: false }); + }, - test(assert) { - assert.ok(queryAll("h1#site-text-logo.text-logo").length === 1); - assert.equal(queryAll("#site-text-logo").text(), title); - }, - }); + test(assert) { + assert.ok(queryAll("h1#site-text-logo.text-logo").length === 1); + assert.equal(queryAll("#site-text-logo").text(), title); + }, + }); - componentTest("no logo - minimized", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = ""; - this.siteSettings.site_logo_small_url = ""; - this.siteSettings.title = title; - this.set("args", { minimized: true }); - }, + componentTest("no logo - minimized", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = ""; + this.siteSettings.site_logo_small_url = ""; + this.siteSettings.title = title; + this.set("args", { minimized: true }); + }, - test(assert) { - assert.ok(queryAll(".d-icon-home").length === 1); - }, - }); + test(assert) { + assert.ok(queryAll(".d-icon-home").length === 1); + }, + }); - componentTest("mobile logo", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_mobile_logo_url = mobileLogo; - this.siteSettings.site_logo_small_url = smallLogo; - this.site.mobileView = true; - }, + componentTest("mobile logo", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_mobile_logo_url = mobileLogo; + this.siteSettings.site_logo_small_url = smallLogo; + this.site.mobileView = true; + }, - test(assert) { - assert.ok(queryAll("img#site-logo.logo-mobile").length === 1); - assert.equal(queryAll("#site-logo").attr("src"), mobileLogo); - }, - }); + test(assert) { + assert.ok(queryAll("img#site-logo.logo-mobile").length === 1); + assert.equal(queryAll("#site-logo").attr("src"), mobileLogo); + }, + }); - componentTest("mobile without logo", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.site.mobileView = true; - }, + componentTest("mobile without logo", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.site.mobileView = true; + }, - test(assert) { - assert.ok(queryAll("img#site-logo.logo-big").length === 1); - assert.equal(queryAll("#site-logo").attr("src"), bigLogo); - }, - }); + test(assert) { + assert.ok(queryAll("img#site-logo.logo-big").length === 1); + assert.equal(queryAll("#site-logo").attr("src"), bigLogo); + }, + }); - componentTest("logo with dark mode alternative", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_logo_dark_url = darkLogo; - Session.currentProp("darkModeAvailable", true); - }, - afterEach() { - Session.currentProp("darkModeAvailable", null); - }, + componentTest("logo with dark mode alternative", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = darkLogo; + Session.currentProp("darkModeAvailable", true); + }, + afterEach() { + Session.currentProp("darkModeAvailable", null); + }, - test(assert) { - assert.ok(queryAll("img#site-logo.logo-big").length === 1); - assert.equal(queryAll("#site-logo").attr("src"), bigLogo); + test(assert) { + assert.ok(queryAll("img#site-logo.logo-big").length === 1); + assert.equal(queryAll("#site-logo").attr("src"), bigLogo); - assert.equal( - queryAll("picture source").attr("media"), - prefersDark, - "includes dark mode media attribute" - ); - assert.equal( - queryAll("picture source").attr("srcset"), - darkLogo, - "includes dark mode alternative logo source" - ); - }, - }); + assert.equal( + queryAll("picture source").attr("media"), + prefersDark, + "includes dark mode media attribute" + ); + assert.equal( + queryAll("picture source").attr("srcset"), + darkLogo, + "includes dark mode alternative logo source" + ); + }, + }); - componentTest("mobile logo with dark mode alternative", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_mobile_logo_url = mobileLogo; - this.siteSettings.site_mobile_logo_dark_url = darkLogo; - Session.currentProp("darkModeAvailable", true); + componentTest("mobile logo with dark mode alternative", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_mobile_logo_url = mobileLogo; + this.siteSettings.site_mobile_logo_dark_url = darkLogo; + Session.currentProp("darkModeAvailable", true); - this.site.mobileView = true; - }, - afterEach() { - Session.currentProp("darkModeAvailable", null); - }, + this.site.mobileView = true; + }, + afterEach() { + Session.currentProp("darkModeAvailable", null); + }, - test(assert) { - assert.equal(queryAll("#site-logo").attr("src"), mobileLogo); + test(assert) { + assert.equal(queryAll("#site-logo").attr("src"), mobileLogo); - assert.equal( - queryAll("picture source").attr("media"), - prefersDark, - "includes dark mode media attribute" - ); - assert.equal( - queryAll("picture source").attr("srcset"), - darkLogo, - "includes dark mode alternative logo source" - ); - }, - }); + assert.equal( + queryAll("picture source").attr("media"), + prefersDark, + "includes dark mode media attribute" + ); + assert.equal( + queryAll("picture source").attr("srcset"), + darkLogo, + "includes dark mode alternative logo source" + ); + }, + }); - componentTest("dark mode enabled but no dark logo set", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_logo_dark_url = ""; - Session.currentProp("darkModeAvailable", true); - }, - afterEach() { - Session.currentProp("darkModeAvailable", null); - }, + componentTest("dark mode enabled but no dark logo set", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = ""; + Session.currentProp("darkModeAvailable", true); + }, + afterEach() { + Session.currentProp("darkModeAvailable", null); + }, - test(assert) { - assert.ok(queryAll("img#site-logo.logo-big").length === 1); - assert.equal(queryAll("#site-logo").attr("src"), bigLogo); - assert.ok( - queryAll("picture").length === 0, - "does not include alternative logo" - ); - }, - }); + test(assert) { + assert.ok(queryAll("img#site-logo.logo-big").length === 1); + assert.equal(queryAll("#site-logo").attr("src"), bigLogo); + assert.ok( + queryAll("picture").length === 0, + "does not include alternative logo" + ); + }, + }); - componentTest("dark logo set but no dark mode", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_logo_dark_url = darkLogo; - }, + componentTest("dark logo set but no dark mode", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = darkLogo; + }, - test(assert) { - assert.ok(queryAll("img#site-logo.logo-big").length === 1); - assert.equal(queryAll("#site-logo").attr("src"), bigLogo); - assert.ok( - queryAll("picture").length === 0, - "does not include alternative logo" - ); - }, - }); + test(assert) { + assert.ok(queryAll("img#site-logo.logo-big").length === 1); + assert.equal(queryAll("#site-logo").attr("src"), bigLogo); + assert.ok( + queryAll("picture").length === 0, + "does not include alternative logo" + ); + }, + }); - componentTest("dark color scheme and dark logo set", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_logo_dark_url = darkLogo; - Session.currentProp("defaultColorSchemeIsDark", true); - }, - afterEach() { - Session.currentProp("defaultColorSchemeIsDark", null); - }, - test(assert) { - assert.ok(queryAll("img#site-logo.logo-big").length === 1); - assert.equal( - queryAll("#site-logo").attr("src"), - darkLogo, - "uses dark logo" - ); - assert.ok( - queryAll("picture").length === 0, - "does not add dark mode alternative" - ); - }, - }); + componentTest("dark color scheme and dark logo set", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = darkLogo; + Session.currentProp("defaultColorSchemeIsDark", true); + }, + afterEach() { + Session.currentProp("defaultColorSchemeIsDark", null); + }, + test(assert) { + assert.ok(queryAll("img#site-logo.logo-big").length === 1); + assert.equal( + queryAll("#site-logo").attr("src"), + darkLogo, + "uses dark logo" + ); + assert.ok( + queryAll("picture").length === 0, + "does not add dark mode alternative" + ); + }, + }); - componentTest("dark color scheme and dark logo not set", { - template: '{{mount-widget widget="home-logo" args=args}}', - beforeEach() { - this.siteSettings.site_logo_url = bigLogo; - this.siteSettings.site_logo_dark_url = ""; - Session.currentProp("defaultColorSchemeIsDark", true); - }, - afterEach() { - Session.currentProp("defaultColorSchemeIsDark", null); - }, - test(assert) { - assert.ok(queryAll("img#site-logo.logo-big").length === 1); - assert.equal( - queryAll("#site-logo").attr("src"), - bigLogo, - "uses regular logo on dark scheme if no dark logo" - ); - }, - }); -}); + componentTest("dark color scheme and dark logo not set", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = ""; + Session.currentProp("defaultColorSchemeIsDark", true); + }, + afterEach() { + Session.currentProp("defaultColorSchemeIsDark", null); + }, + test(assert) { + assert.ok(queryAll("img#site-logo.logo-big").length === 1); + assert.equal( + queryAll("#site-logo").attr("src"), + bigLogo, + "uses regular logo on dark scheme if no dark logo" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/post-links-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/post-links-test.js index 6a24877b12..e8287519c8 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/post-links-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/post-links-test.js @@ -7,59 +7,91 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import { click } from "@ember/test-helpers"; -discourseModule("Integration | Component | Widget | post-links", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | post-links", + function (hooks) { + setupRenderingTest(hooks); - componentTest("duplicate links", { - template: '{{mount-widget widget="post-links" args=args}}', - beforeEach() { - this.set("args", { - id: 2, - links: [ - { - title: "Evil Trout Link", - url: "http://eviltrout.com", - reflection: true, - }, - { - title: "Evil Trout Link", - url: "http://dupe.eviltrout.com", - reflection: true, - }, - ], - }); - }, - test(assert) { - assert.equal( - queryAll(".post-links a.track-link").length, - 1, - "it hides the dupe link" - ); - }, - }); + componentTest("duplicate links", { + template: '{{mount-widget widget="post-links" args=args}}', + beforeEach() { + this.set("args", { + id: 2, + links: [ + { + title: "Evil Trout Link", + url: "http://eviltrout.com", + reflection: true, + }, + { + title: "Evil Trout Link", + url: "http://dupe.eviltrout.com", + reflection: true, + }, + ], + }); + }, + test(assert) { + assert.equal( + queryAll(".post-links a.track-link").length, + 1, + "it hides the dupe link" + ); + }, + }); - componentTest("collapsed links", { - template: '{{mount-widget widget="post-links" args=args}}', - beforeEach() { - this.set("args", { - id: 1, - links: [ - { title: "Link 1", url: "http://eviltrout.com?1", reflection: true }, - { title: "Link 2", url: "http://eviltrout.com?2", reflection: true }, - { title: "Link 3", url: "http://eviltrout.com?3", reflection: true }, - { title: "Link 4", url: "http://eviltrout.com?4", reflection: true }, - { title: "Link 5", url: "http://eviltrout.com?5", reflection: true }, - { title: "Link 6", url: "http://eviltrout.com?6", reflection: true }, - { title: "Link 7", url: "http://eviltrout.com?7", reflection: true }, - ], - }); - }, - async test(assert) { - assert.ok(queryAll(".expand-links").length === 1, "collapsed by default"); - await click("a.expand-links"); - assert.equal(queryAll(".post-links a.track-link").length, 7); - }, - }); -}); + componentTest("collapsed links", { + template: '{{mount-widget widget="post-links" args=args}}', + beforeEach() { + this.set("args", { + id: 1, + links: [ + { + title: "Link 1", + url: "http://eviltrout.com?1", + reflection: true, + }, + { + title: "Link 2", + url: "http://eviltrout.com?2", + reflection: true, + }, + { + title: "Link 3", + url: "http://eviltrout.com?3", + reflection: true, + }, + { + title: "Link 4", + url: "http://eviltrout.com?4", + reflection: true, + }, + { + title: "Link 5", + url: "http://eviltrout.com?5", + reflection: true, + }, + { + title: "Link 6", + url: "http://eviltrout.com?6", + reflection: true, + }, + { + title: "Link 7", + url: "http://eviltrout.com?7", + reflection: true, + }, + ], + }); + }, + async test(assert) { + assert.ok( + queryAll(".expand-links").length === 1, + "collapsed by default" + ); + await click("a.expand-links"); + assert.equal(queryAll(".post-links a.track-link").length, 7); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/post-menu-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/post-menu-test.js index a497cae817..53f10c31fe 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/post-menu-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/post-menu-test.js @@ -7,48 +7,49 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import { withPluginApi } from "discourse/lib/plugin-api"; -discourseModule("Integration | Component | Widget | post-menu", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | post-menu", + function (hooks) { + setupRenderingTest(hooks); - componentTest("add extra button", { - template: '{{mount-widget widget="post-menu" args=args}}', - beforeEach() { - this.set("args", {}); - withPluginApi("0.8", (api) => { - api.addPostMenuButton("coffee", () => { - return { - action: "drinkCoffee", - icon: "coffee", - className: "hot-coffee", - title: "coffee.title", - position: "first", - }; + componentTest("add extra button", { + template: '{{mount-widget widget="post-menu" args=args}}', + beforeEach() { + this.set("args", {}); + withPluginApi("0.8", (api) => { + api.addPostMenuButton("coffee", () => { + return { + action: "drinkCoffee", + icon: "coffee", + className: "hot-coffee", + title: "coffee.title", + position: "first", + }; + }); }); - }); - }, - async test(assert) { - assert.ok( - queryAll(".actions .extra-buttons .hot-coffee").length === 1, - "It renders extra button" - ); - }, - }); + }, + async test(assert) { + assert.ok( + queryAll(".actions .extra-buttons .hot-coffee").length === 1, + "It renders extra button" + ); + }, + }); - componentTest("remove extra button", { - template: '{{mount-widget widget="post-menu" args=args}}', - beforeEach() { - this.set("args", {}); - withPluginApi("0.8", (api) => { - api.removePostMenuButton("coffee"); - }); - }, - async test(assert) { - assert.ok( - queryAll(".actions .extra-buttons .hot-coffee").length === 0, - "It doesn't removes coffee button" - ); - }, - }); -}); + componentTest("remove extra button", { + template: '{{mount-widget widget="post-menu" args=args}}', + beforeEach() { + this.set("args", {}); + withPluginApi("0.8", (api) => { + api.removePostMenuButton("coffee"); + }); + }, + async test(assert) { + assert.ok( + queryAll(".actions .extra-buttons .hot-coffee").length === 0, + "It doesn't removes coffee button" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/post-stream-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/post-stream-test.js index 5d4ce3ff92..1005000f04 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/post-stream-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/post-stream-test.js @@ -21,136 +21,137 @@ function postStreamTest(name, attrs) { }); } -discourseModule("Integration | Component | Widget | post-stream", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | post-stream", + function (hooks) { + setupRenderingTest(hooks); - postStreamTest("basics", { - posts() { - const site = this.container.lookup("site:main"); - const topic = Topic.create(); - topic.set("details.created_by", { id: 123 }); - return [ - Post.create({ - topic, - id: 1, - post_number: 1, - user_id: 123, - primary_group_name: "trout", - avatar_template: "/images/avatar.png", - }), - Post.create({ - topic, - id: 2, - post_number: 2, - post_type: site.get("post_types.moderator_action"), - }), - Post.create({ topic, id: 3, post_number: 3, hidden: true }), - Post.create({ - topic, - id: 4, - post_number: 4, - post_type: site.get("post_types.whisper"), - }), - Post.create({ - topic, - id: 5, - post_number: 5, - wiki: true, - via_email: true, - }), - Post.create({ - topic, - id: 6, - post_number: 6, - via_email: true, - is_auto_generated: true, - }), - ]; - }, + postStreamTest("basics", { + posts() { + const site = this.container.lookup("site:main"); + const topic = Topic.create(); + topic.set("details.created_by", { id: 123 }); + return [ + Post.create({ + topic, + id: 1, + post_number: 1, + user_id: 123, + primary_group_name: "trout", + avatar_template: "/images/avatar.png", + }), + Post.create({ + topic, + id: 2, + post_number: 2, + post_type: site.get("post_types.moderator_action"), + }), + Post.create({ topic, id: 3, post_number: 3, hidden: true }), + Post.create({ + topic, + id: 4, + post_number: 4, + post_type: site.get("post_types.whisper"), + }), + Post.create({ + topic, + id: 5, + post_number: 5, + wiki: true, + via_email: true, + }), + Post.create({ + topic, + id: 6, + post_number: 6, + via_email: true, + is_auto_generated: true, + }), + ]; + }, - test(assert) { - assert.equal(queryAll(".post-stream").length, 1); - assert.equal(queryAll(".topic-post").length, 6, "renders all posts"); + test(assert) { + assert.equal(queryAll(".post-stream").length, 1); + assert.equal(queryAll(".topic-post").length, 6, "renders all posts"); - // look for special class bindings - assert.equal( - queryAll(".topic-post:nth-of-type(1).topic-owner").length, - 1, - "it applies the topic owner class" - ); - assert.equal( - queryAll(".topic-post:nth-of-type(1).group-trout").length, - 1, - "it applies the primary group class" - ); - assert.equal( - queryAll(".topic-post:nth-of-type(1).regular").length, - 1, - "it applies the regular class" - ); - assert.equal( - queryAll(".topic-post:nth-of-type(2).moderator").length, - 1, - "it applies the moderator class" - ); - assert.equal( - queryAll(".topic-post:nth-of-type(3).post-hidden").length, - 1, - "it applies the hidden class" - ); - assert.equal( - queryAll(".topic-post:nth-of-type(4).whisper").length, - 1, - "it applies the whisper class" - ); - assert.equal( - queryAll(".topic-post:nth-of-type(5).wiki").length, - 1, - "it applies the wiki class" - ); + // look for special class bindings + assert.equal( + queryAll(".topic-post:nth-of-type(1).topic-owner").length, + 1, + "it applies the topic owner class" + ); + assert.equal( + queryAll(".topic-post:nth-of-type(1).group-trout").length, + 1, + "it applies the primary group class" + ); + assert.equal( + queryAll(".topic-post:nth-of-type(1).regular").length, + 1, + "it applies the regular class" + ); + assert.equal( + queryAll(".topic-post:nth-of-type(2).moderator").length, + 1, + "it applies the moderator class" + ); + assert.equal( + queryAll(".topic-post:nth-of-type(3).post-hidden").length, + 1, + "it applies the hidden class" + ); + assert.equal( + queryAll(".topic-post:nth-of-type(4).whisper").length, + 1, + "it applies the whisper class" + ); + assert.equal( + queryAll(".topic-post:nth-of-type(5).wiki").length, + 1, + "it applies the wiki class" + ); - // it renders an article for the body with appropriate attributes - assert.equal(queryAll("article#post_2").length, 1); - assert.equal(queryAll('article[data-user-id="123"]').length, 1); - assert.equal(queryAll('article[data-post-id="3"]').length, 1); - assert.equal(queryAll("article#post_5.via-email").length, 1); - assert.equal(queryAll("article#post_6.is-auto-generated").length, 1); + // it renders an article for the body with appropriate attributes + assert.equal(queryAll("article#post_2").length, 1); + assert.equal(queryAll('article[data-user-id="123"]').length, 1); + assert.equal(queryAll('article[data-post-id="3"]').length, 1); + assert.equal(queryAll("article#post_5.via-email").length, 1); + assert.equal(queryAll("article#post_6.is-auto-generated").length, 1); - assert.equal( - queryAll("article:nth-of-type(1) .main-avatar").length, - 1, - "renders the main avatar" - ); - }, - }); + assert.equal( + queryAll("article:nth-of-type(1) .main-avatar").length, + 1, + "renders the main avatar" + ); + }, + }); - postStreamTest("deleted posts", { - posts() { - const topic = Topic.create(); - topic.set("details.created_by", { id: 123 }); - return [ - Post.create({ - topic, - id: 1, - post_number: 1, - deleted_at: new Date().toString(), - }), - ]; - }, + postStreamTest("deleted posts", { + posts() { + const topic = Topic.create(); + topic.set("details.created_by", { id: 123 }); + return [ + Post.create({ + topic, + id: 1, + post_number: 1, + deleted_at: new Date().toString(), + }), + ]; + }, - test(assert) { - assert.equal( - queryAll(".topic-post.deleted").length, - 1, - "it applies the deleted class" - ); - assert.equal( - queryAll(".deleted-user-avatar").length, - 1, - "it has the trash avatar" - ); - }, - }); -}); + test(assert) { + assert.equal( + queryAll(".topic-post.deleted").length, + 1, + "it applies the deleted class" + ); + assert.equal( + queryAll(".deleted-user-avatar").length, + 1, + "it has the trash avatar" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js index 9206c230eb..5f74973677 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js @@ -113,7 +113,7 @@ discourseModule("Integration | Component | Widget | post", function (hooks) { this.on("showHistory", () => (this.historyShown = true)); }, async test(assert) { - await click(".post-info.edits"); + await click(".post-info.edits button"); assert.ok(this.historyShown, "clicking the pencil shows the history"); }, }); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/poster-name-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/poster-name-test.js index 0c2a15ba76..725ba46060 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/poster-name-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/poster-name-test.js @@ -6,74 +6,75 @@ import { queryAll, } from "discourse/tests/helpers/qunit-helpers"; -discourseModule("Integration | Component | Widget | poster-name", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | poster-name", + function (hooks) { + setupRenderingTest(hooks); - componentTest("basic rendering", { - template: '{{mount-widget widget="poster-name" args=args}}', - beforeEach() { - this.set("args", { - username: "eviltrout", - usernameUrl: "/u/eviltrout", - name: "Robin Ward", - user_title: "Trout Master", - }); - }, - test(assert) { - assert.ok(queryAll(".names").length); - assert.ok(queryAll("span.username").length); - assert.ok(queryAll('a[data-user-card="eviltrout"]').length); - assert.equal(queryAll(".username a").text(), "eviltrout"); - assert.equal(queryAll(".full-name a").text(), "Robin Ward"); - assert.equal(queryAll(".user-title").text(), "Trout Master"); - }, - }); + componentTest("basic rendering", { + template: '{{mount-widget widget="poster-name" args=args}}', + beforeEach() { + this.set("args", { + username: "eviltrout", + usernameUrl: "/u/eviltrout", + name: "Robin Ward", + user_title: "Trout Master", + }); + }, + test(assert) { + assert.ok(queryAll(".names").length); + assert.ok(queryAll("span.username").length); + assert.ok(queryAll('a[data-user-card="eviltrout"]').length); + assert.equal(queryAll(".username a").text(), "eviltrout"); + assert.equal(queryAll(".full-name a").text(), "Robin Ward"); + assert.equal(queryAll(".user-title").text(), "Trout Master"); + }, + }); - componentTest("extra classes and glyphs", { - template: '{{mount-widget widget="poster-name" args=args}}', - beforeEach() { - this.set("args", { - username: "eviltrout", - usernameUrl: "/u/eviltrout", - staff: true, - admin: true, - moderator: true, - new_user: true, - primary_group_name: "fish", - }); - }, - test(assert) { - assert.ok(queryAll("span.staff").length); - assert.ok(queryAll("span.admin").length); - assert.ok(queryAll("span.moderator").length); - assert.ok(queryAll(".d-icon-shield-alt").length); - assert.ok(queryAll("span.new-user").length); - assert.ok(queryAll("span.fish").length); - }, - }); + componentTest("extra classes and glyphs", { + template: '{{mount-widget widget="poster-name" args=args}}', + beforeEach() { + this.set("args", { + username: "eviltrout", + usernameUrl: "/u/eviltrout", + staff: true, + admin: true, + moderator: true, + new_user: true, + primary_group_name: "fish", + }); + }, + test(assert) { + assert.ok(queryAll("span.staff").length); + assert.ok(queryAll("span.admin").length); + assert.ok(queryAll("span.moderator").length); + assert.ok(queryAll(".d-icon-shield-alt").length); + assert.ok(queryAll("span.new-user").length); + assert.ok(queryAll("span.fish").length); + }, + }); - componentTest("disable display name on posts", { - template: '{{mount-widget widget="poster-name" args=args}}', - beforeEach() { - this.siteSettings.display_name_on_posts = false; - this.set("args", { username: "eviltrout", name: "Robin Ward" }); - }, - test(assert) { - assert.equal(queryAll(".full-name").length, 0); - }, - }); + componentTest("disable display name on posts", { + template: '{{mount-widget widget="poster-name" args=args}}', + beforeEach() { + this.siteSettings.display_name_on_posts = false; + this.set("args", { username: "eviltrout", name: "Robin Ward" }); + }, + test(assert) { + assert.equal(queryAll(".full-name").length, 0); + }, + }); - componentTest("doesn't render a name if it's similar to the username", { - template: '{{mount-widget widget="poster-name" args=args}}', - beforeEach() { - this.siteSettings.prioritize_username_in_ux = true; - this.siteSettings.display_name_on_posts = true; - this.set("args", { username: "eviltrout", name: "evil-trout" }); - }, - test(assert) { - assert.equal(queryAll(".second").length, 0); - }, - }); -}); + componentTest("doesn't render a name if it's similar to the username", { + template: '{{mount-widget widget="poster-name" args=args}}', + beforeEach() { + this.siteSettings.prioritize_username_in_ux = true; + this.siteSettings.display_name_on_posts = true; + this.set("args", { username: "eviltrout", name: "evil-trout" }); + }, + test(assert) { + assert.equal(queryAll(".second").length, 0); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/small-user-list-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/small-user-list-test.js index 20ae8b1206..95316703f7 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/small-user-list-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/small-user-list-test.js @@ -6,25 +6,26 @@ import { queryAll, } from "discourse/tests/helpers/qunit-helpers"; -discourseModule("Integration | Component | Widget | small-user-list", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | small-user-list", + function (hooks) { + setupRenderingTest(hooks); - componentTest("renders avatars and support for unknown", { - template: '{{mount-widget widget="small-user-list" args=args}}', - beforeEach() { - this.set("args", { - users: [ - { id: 456, username: "eviltrout" }, - { id: 457, username: "someone", unknown: true }, - ], - }); - }, - async test(assert) { - assert.ok(queryAll('[data-user-card="eviltrout"]').length === 1); - assert.ok(queryAll('[data-user-card="someone"]').length === 0); - assert.ok(queryAll(".unknown").length, "includes unkown user"); - }, - }); -}); + componentTest("renders avatars and support for unknown", { + template: '{{mount-widget widget="small-user-list" args=args}}', + beforeEach() { + this.set("args", { + users: [ + { id: 456, username: "eviltrout" }, + { id: 457, username: "someone", unknown: true }, + ], + }); + }, + async test(assert) { + assert.ok(queryAll('[data-user-card="eviltrout"]').length === 1); + assert.ok(queryAll('[data-user-card="someone"]').length === 0); + assert.ok(queryAll(".unknown").length, "includes unkown user"); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/topic-status-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/topic-status-test.js index 89fce7aeb1..f8e8377eb0 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/topic-status-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/topic-status-test.js @@ -7,41 +7,42 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import TopicStatusIcons from "discourse/helpers/topic-status-icons"; -discourseModule("Integration | Component | Widget | topic-status", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | topic-status", + function (hooks) { + setupRenderingTest(hooks); - componentTest("basics", { - template: '{{mount-widget widget="topic-status" args=args}}', - beforeEach(store) { - this.set("args", { - topic: store.createRecord("topic", { closed: true }), - disableActions: true, - }); - }, - test(assert) { - assert.ok(queryAll(".topic-status .d-icon-lock").length); - }, - }); + componentTest("basics", { + template: '{{mount-widget widget="topic-status" args=args}}', + beforeEach(store) { + this.set("args", { + topic: store.createRecord("topic", { closed: true }), + disableActions: true, + }); + }, + test(assert) { + assert.ok(queryAll(".topic-status .d-icon-lock").length); + }, + }); - componentTest("extendability", { - template: '{{mount-widget widget="topic-status" args=args}}', - beforeEach(store) { - TopicStatusIcons.addObject([ - "has_accepted_answer", - "far-check-square", - "solved", - ]); - this.set("args", { - topic: store.createRecord("topic", { - has_accepted_answer: true, - }), - disableActions: true, - }); - }, - test(assert) { - assert.ok(queryAll(".topic-status .d-icon-far-check-square").length); - }, - }); -}); + componentTest("extendability", { + template: '{{mount-widget widget="topic-status" args=args}}', + beforeEach(store) { + TopicStatusIcons.addObject([ + "has_accepted_answer", + "far-check-square", + "solved", + ]); + this.set("args", { + topic: store.createRecord("topic", { + has_accepted_answer: true, + }), + disableActions: true, + }); + }, + test(assert) { + assert.ok(queryAll(".topic-status .d-icon-far-check-square").length); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js index 891c07101c..232745f935 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js @@ -10,224 +10,227 @@ import I18n from "I18n"; import { click } from "@ember/test-helpers"; import sinon from "sinon"; -discourseModule("Integration | Component | Widget | user-menu", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | user-menu", + function (hooks) { + setupRenderingTest(hooks); - componentTest("basics", { - template: '{{mount-widget widget="user-menu"}}', + componentTest("basics", { + template: '{{mount-widget widget="user-menu"}}', - test(assert) { - assert.ok(queryAll(".user-menu").length); - assert.ok(queryAll(".user-preferences-link").length); - assert.ok(queryAll(".user-notifications-link").length); - assert.ok(queryAll(".user-bookmarks-link").length); - assert.ok(queryAll(".quick-access-panel").length); - assert.ok(queryAll(".notifications-dismiss").length); - }, - }); + test(assert) { + assert.ok(queryAll(".user-menu").length); + assert.ok(queryAll(".user-preferences-link").length); + assert.ok(queryAll(".user-notifications-link").length); + assert.ok(queryAll(".user-bookmarks-link").length); + assert.ok(queryAll(".quick-access-panel").length); + assert.ok(queryAll(".notifications-dismiss").length); + }, + }); - componentTest("notifications", { - template: '{{mount-widget widget="user-menu"}}', + componentTest("notifications", { + template: '{{mount-widget widget="user-menu"}}', - async test(assert) { - const $links = queryAll(".quick-access-panel li a"); + async test(assert) { + const $links = queryAll(".quick-access-panel li a"); - assert.equal($links.length, 5); - assert.ok($links[0].href.includes("/t/a-slug/123")); + assert.equal($links.length, 5); + assert.ok($links[0].href.includes("/t/a-slug/123")); - assert.ok( - $links[1].href.includes( - "/u/eviltrout/notifications/likes-received?acting_username=aquaman" - ) - ); + assert.ok( + $links[1].href.includes( + "/u/eviltrout/notifications/likes-received?acting_username=aquaman" + ) + ); - assert.equal( - $links[1].text, - `aquaman ${I18n.t("notifications.liked_consolidated_description", { - count: 5, - })}` - ); - - assert.ok($links[2].href.includes("/u/test2/messages/group/test")); - assert.ok( - $links[2].innerHTML.includes( - I18n.t("notifications.group_message_summary", { + assert.equal( + $links[1].text, + `aquaman ${I18n.t("notifications.liked_consolidated_description", { count: 5, - group_name: "test", - }) - ) - ); + })}` + ); - assert.ok($links[3].href.includes("/u/test1")); - assert.ok( - $links[3].innerHTML.includes( - I18n.t("notifications.invitee_accepted", { username: "test1" }) - ) - ); + assert.ok($links[2].href.includes("/u/test2/messages/group/test")); + assert.ok( + $links[2].innerHTML.includes( + I18n.t("notifications.group_message_summary", { + count: 5, + group_name: "test", + }) + ) + ); - assert.ok($links[4].href.includes("/g/test")); - assert.ok( - $links[4].innerHTML.includes( - I18n.t("notifications.membership_request_accepted", { - group_name: "test", - }) - ) - ); + assert.ok($links[3].href.includes("/u/test1")); + assert.ok( + $links[3].innerHTML.includes( + I18n.t("notifications.invitee_accepted", { username: "test1" }) + ) + ); - const routeToStub = sinon.stub(DiscourseURL, "routeTo"); - await click(".user-notifications-link"); - assert.ok( - routeToStub.calledWith(queryAll(".user-notifications-link")[0].href), - "a second click should redirect to the full notifications page" - ); - }, - }); + assert.ok($links[4].href.includes("/g/test")); + assert.ok( + $links[4].innerHTML.includes( + I18n.t("notifications.membership_request_accepted", { + group_name: "test", + }) + ) + ); - componentTest("log out", { - template: '{{mount-widget widget="user-menu" logout=(action "logout")}}', + const routeToStub = sinon.stub(DiscourseURL, "routeTo"); + await click(".user-notifications-link"); + assert.ok( + routeToStub.calledWith( + queryAll(".user-notifications-link").data("url") + ), + "a second click should redirect to the full notifications page" + ); + }, + }); - beforeEach() { - this.on("logout", () => (this.loggedOut = true)); - }, + componentTest("log out", { + template: '{{mount-widget widget="user-menu" logout=(action "logout")}}', - async test(assert) { - await click(".user-preferences-link"); + beforeEach() { + this.on("logout", () => (this.loggedOut = true)); + }, - assert.ok(queryAll(".logout").length); + async test(assert) { + await click(".user-preferences-link"); - await click(".logout button"); - assert.ok(this.loggedOut); - }, - }); + assert.ok(queryAll(".logout").length); - componentTest("private messages - disabled", { - template: '{{mount-widget widget="user-menu"}}', - beforeEach() { - this.siteSettings.enable_personal_messages = false; - }, + await click(".logout button"); + assert.ok(this.loggedOut); + }, + }); - test(assert) { - assert.ok(!queryAll(".user-pms-link").length); - }, - }); + componentTest("private messages - disabled", { + template: '{{mount-widget widget="user-menu"}}', + beforeEach() { + this.siteSettings.enable_personal_messages = false; + }, - componentTest("private messages - enabled", { - template: '{{mount-widget widget="user-menu"}}', - beforeEach() { - this.siteSettings.enable_personal_messages = true; - }, + test(assert) { + assert.ok(!queryAll(".user-pms-link").length); + }, + }); - async test(assert) { - const userPmsLink = queryAll(".user-pms-link")[0]; - assert.ok(userPmsLink); - await click(".user-pms-link"); + componentTest("private messages - enabled", { + template: '{{mount-widget widget="user-menu"}}', + beforeEach() { + this.siteSettings.enable_personal_messages = true; + }, - const message = queryAll(".quick-access-panel li a")[0]; - assert.ok(message); + async test(assert) { + const userPmsLink = queryAll(".user-pms-link").data("url"); + assert.ok(userPmsLink); + await click(".user-pms-link"); - assert.ok( - message.href.includes("/t/bug-can-not-render-emoji-properly/174/2"), - "should link to the next unread post" - ); - assert.ok( - message.innerHTML.includes("mixtape"), - "should include the last poster's username" - ); - assert.ok( - message.innerHTML.match(//), - "should correctly render emoji in message title" - ); + const message = queryAll(".quick-access-panel li a")[0]; + assert.ok(message); - const routeToStub = sinon.stub(DiscourseURL, "routeTo"); - await click(".user-pms-link"); - assert.ok( - routeToStub.calledWith(userPmsLink.href), - "a second click should redirect to the full private messages page" - ); - }, - }); + assert.ok( + message.href.includes("/t/bug-can-not-render-emoji-properly/174/2"), + "should link to the next unread post" + ); + assert.ok( + message.innerHTML.includes("mixtape"), + "should include the last poster's username" + ); + assert.ok( + message.innerHTML.match(//), + "should correctly render emoji in message title" + ); - componentTest("bookmarks", { - template: '{{mount-widget widget="user-menu"}}', + const routeToStub = sinon.stub(DiscourseURL, "routeTo"); + await click(".user-pms-link"); + assert.ok( + routeToStub.calledWith(userPmsLink), + "a second click should redirect to the full private messages page" + ); + }, + }); - async test(assert) { - await click(".user-bookmarks-link"); + componentTest("bookmarks", { + template: '{{mount-widget widget="user-menu"}}', - const bookmark = queryAll(".quick-access-panel li a")[0]; - assert.ok(bookmark); + async test(assert) { + await click(".user-bookmarks-link"); - assert.ok(bookmark.href.includes("/t/yelling-topic-title/119")); - assert.ok( - bookmark.innerHTML.includes("someguy"), - "should include the last poster's username" - ); - assert.ok( - bookmark.innerHTML.match(//), - "should correctly render emoji in bookmark title" - ); + const bookmark = queryAll(".quick-access-panel li a")[0]; + assert.ok(bookmark); - const routeToStub = sinon.stub(DiscourseURL, "routeTo"); - await click(".user-bookmarks-link"); - assert.ok( - routeToStub.calledWith(queryAll(".user-bookmarks-link")[0].href), - "a second click should redirect to the full bookmarks page" - ); - }, - }); + assert.ok(bookmark.href.includes("/t/yelling-topic-title/119")); + assert.ok( + bookmark.innerHTML.includes("someguy"), + "should include the last poster's username" + ); + assert.ok( + bookmark.innerHTML.match(//), + "should correctly render emoji in bookmark title" + ); - componentTest("anonymous", { - template: - '{{mount-widget widget="user-menu" toggleAnonymous=(action "toggleAnonymous")}}', + const routeToStub = sinon.stub(DiscourseURL, "routeTo"); + await click(".user-bookmarks-link"); + assert.ok( + routeToStub.calledWith(queryAll(".user-bookmarks-link").data("url")), + "a second click should redirect to the full bookmarks page" + ); + }, + }); - beforeEach() { - this.currentUser.setProperties({ is_anonymous: false, trust_level: 3 }); - this.siteSettings.allow_anonymous_posting = true; - this.siteSettings.anonymous_posting_min_trust_level = 3; + componentTest("anonymous", { + template: + '{{mount-widget widget="user-menu" toggleAnonymous=(action "toggleAnonymous")}}', - this.on("toggleAnonymous", () => (this.anonymous = true)); - }, + beforeEach() { + this.currentUser.setProperties({ is_anonymous: false, trust_level: 3 }); + this.siteSettings.allow_anonymous_posting = true; + this.siteSettings.anonymous_posting_min_trust_level = 3; - async test(assert) { - await click(".user-preferences-link"); - assert.ok(queryAll(".enable-anonymous").length); + this.on("toggleAnonymous", () => (this.anonymous = true)); + }, - await click(".enable-anonymous"); - assert.ok(this.anonymous); - }, - }); + async test(assert) { + await click(".user-preferences-link"); + assert.ok(queryAll(".enable-anonymous").length); - componentTest("anonymous - disabled", { - template: '{{mount-widget widget="user-menu"}}', + await click(".enable-anonymous"); + assert.ok(this.anonymous); + }, + }); - beforeEach() { - this.siteSettings.allow_anonymous_posting = false; - }, + componentTest("anonymous - disabled", { + template: '{{mount-widget widget="user-menu"}}', - async test(assert) { - await click(".user-preferences-link"); - assert.ok(!queryAll(".enable-anonymous").length); - }, - }); + beforeEach() { + this.siteSettings.allow_anonymous_posting = false; + }, - componentTest("anonymous - switch back", { - template: - '{{mount-widget widget="user-menu" toggleAnonymous=(action "toggleAnonymous")}}', + async test(assert) { + await click(".user-preferences-link"); + assert.ok(!queryAll(".enable-anonymous").length); + }, + }); - beforeEach() { - this.currentUser.setProperties({ is_anonymous: true }); - this.siteSettings.allow_anonymous_posting = true; + componentTest("anonymous - switch back", { + template: + '{{mount-widget widget="user-menu" toggleAnonymous=(action "toggleAnonymous")}}', - this.on("toggleAnonymous", () => (this.anonymous = false)); - }, + beforeEach() { + this.currentUser.setProperties({ is_anonymous: true }); + this.siteSettings.allow_anonymous_posting = true; - async test(assert) { - await click(".user-preferences-link"); - assert.ok(queryAll(".disable-anonymous").length); + this.on("toggleAnonymous", () => (this.anonymous = false)); + }, - await click(".disable-anonymous"); - assert.notOk(this.anonymous); - }, - }); -}); + async test(assert) { + await click(".user-preferences-link"); + assert.ok(queryAll(".disable-anonymous").length); + + await click(".disable-anonymous"); + assert.notOk(this.anonymous); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/widget-dropdown-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/widget-dropdown-test.js index 9e2642a16a..f17e92981c 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/widget-dropdown-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/widget-dropdown-test.js @@ -60,79 +60,79 @@ const TEMPLATE = ` ) }}`; -discourseModule("Integration | Component | Widget | widget-dropdown", function ( - hooks -) { - setupRenderingTest(hooks); +discourseModule( + "Integration | Component | Widget | widget-dropdown", + function (hooks) { + setupRenderingTest(hooks); - componentTest("dropdown id", { - template: TEMPLATE, + componentTest("dropdown id", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + }, - test(assert) { - assert.ok(exists("#my-dropdown")); - }, - }); + test(assert) { + assert.ok(exists("#my-dropdown")); + }, + }); - componentTest("label", { - template: TEMPLATE, + componentTest("label", { + template: TEMPLATE, - _translations: I18n.translations, + _translations: I18n.translations, - beforeEach() { - I18n.translations = { en: { js: { foo: "FooBaz" } } }; - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + I18n.translations = { en: { js: { foo: "FooBaz" } } }; + this.setProperties(DEFAULT_CONTENT); + }, - afterEach() { - I18n.translations = this._translations; - }, + afterEach() { + I18n.translations = this._translations; + }, - test(assert) { - assert.equal(headerLabel(), "FooBaz"); - }, - }); + test(assert) { + assert.equal(headerLabel(), "FooBaz"); + }, + }); - componentTest("translatedLabel", { - template: TEMPLATE, + componentTest("translatedLabel", { + template: TEMPLATE, - _translations: I18n.translations, + _translations: I18n.translations, - beforeEach() { - I18n.translations = { en: { js: { foo: "FooBaz" } } }; - this.setProperties(DEFAULT_CONTENT); - this.set("translatedLabel", "BazFoo"); - }, + beforeEach() { + I18n.translations = { en: { js: { foo: "FooBaz" } } }; + this.setProperties(DEFAULT_CONTENT); + this.set("translatedLabel", "BazFoo"); + }, - afterEach() { - I18n.translations = this._translations; - }, + afterEach() { + I18n.translations = this._translations; + }, - test(assert) { - assert.equal(headerLabel(), this.translatedLabel); - }, - }); + test(assert) { + assert.equal(headerLabel(), this.translatedLabel); + }, + }); - componentTest("content", { - template: TEMPLATE, + componentTest("content", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + }, - async test(assert) { - await toggle(); - assert.equal(rowById(1).dataset.id, 1, "it creates rows"); - assert.equal(rowById(2).dataset.id, 2, "it creates rows"); - assert.equal(rowById(3).dataset.id, 3, "it creates rows"); - }, - }); + async test(assert) { + await toggle(); + assert.equal(rowById(1).dataset.id, 1, "it creates rows"); + assert.equal(rowById(2).dataset.id, 2, "it creates rows"); + assert.equal(rowById(3).dataset.id, 3, "it creates rows"); + }, + }); - componentTest("onChange action", { - template: ` + componentTest("onChange action", { + template: `
    {{mount-widget widget="widget-dropdown" @@ -145,204 +145,205 @@ discourseModule("Integration | Component | Widget | widget-dropdown", function ( }} `, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); + beforeEach() { + this.setProperties(DEFAULT_CONTENT); - this.on( - "onChange", - (item) => (this._element.querySelector("#test").innerText = item.id) - ); - }, + this.on( + "onChange", + (item) => (this._element.querySelector("#test").innerText = item.id) + ); + }, - async test(assert) { - await toggle(); - await clickRowById(2); - assert.equal( - queryAll("#test").text(), - 2, - "it calls the onChange actions" - ); - }, - }); + async test(assert) { + await toggle(); + await clickRowById(2); + assert.equal( + queryAll("#test").text(), + 2, + "it calls the onChange actions" + ); + }, + }); - componentTest("can be opened and closed", { - template: TEMPLATE, + componentTest("can be opened and closed", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + }, - async test(assert) { - assert.ok(exists("#my-dropdown.closed")); - assert.ok(!exists("#my-dropdown .widget-dropdown-body")); - await toggle(); - assert.equal(rowById(2).innerText.trim(), "FooBar"); - assert.ok(exists("#my-dropdown.opened")); - assert.ok(exists("#my-dropdown .widget-dropdown-body")); - await toggle(); - assert.ok(exists("#my-dropdown.closed")); - assert.ok(!exists("#my-dropdown .widget-dropdown-body")); - }, - }); + async test(assert) { + assert.ok(exists("#my-dropdown.closed")); + assert.ok(!exists("#my-dropdown .widget-dropdown-body")); + await toggle(); + assert.equal(rowById(2).innerText.trim(), "FooBar"); + assert.ok(exists("#my-dropdown.opened")); + assert.ok(exists("#my-dropdown .widget-dropdown-body")); + await toggle(); + assert.ok(exists("#my-dropdown.closed")); + assert.ok(!exists("#my-dropdown .widget-dropdown-body")); + }, + }); - componentTest("icon", { - template: TEMPLATE, + componentTest("icon", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - this.set("icon", "times"); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + this.set("icon", "times"); + }, - test(assert) { - assert.ok(exists(header().querySelector(".d-icon-times"))); - }, - }); + test(assert) { + assert.ok(exists(header().querySelector(".d-icon-times"))); + }, + }); - componentTest("class", { - template: TEMPLATE, + componentTest("class", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - this.set("class", "activated"); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + this.set("class", "activated"); + }, - test(assert) { - assert.ok(exists("#my-dropdown.activated")); - }, - }); + test(assert) { + assert.ok(exists("#my-dropdown.activated")); + }, + }); - componentTest("content with translatedLabel", { - template: TEMPLATE, + componentTest("content with translatedLabel", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + }, - async test(assert) { - await toggle(); - assert.equal(rowById(2).innerText.trim(), "FooBar"); - }, - }); + async test(assert) { + await toggle(); + assert.equal(rowById(2).innerText.trim(), "FooBar"); + }, + }); - componentTest("content with label", { - template: TEMPLATE, + componentTest("content with label", { + template: TEMPLATE, - _translations: I18n.translations, + _translations: I18n.translations, - beforeEach() { - I18n.translations = { en: { js: { foo: "FooBaz" } } }; - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + I18n.translations = { en: { js: { foo: "FooBaz" } } }; + this.setProperties(DEFAULT_CONTENT); + }, - afterEach() { - I18n.translations = this._translations; - }, + afterEach() { + I18n.translations = this._translations; + }, - async test(assert) { - await toggle(); - assert.equal(rowById(1).innerText.trim(), "FooBaz"); - }, - }); + async test(assert) { + await toggle(); + assert.equal(rowById(1).innerText.trim(), "FooBaz"); + }, + }); - componentTest("content with icon", { - template: TEMPLATE, + componentTest("content with icon", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + }, - async test(assert) { - await toggle(); - assert.ok(exists(rowById(3).querySelector(".d-icon-times"))); - }, - }); + async test(assert) { + await toggle(); + assert.ok(exists(rowById(3).querySelector(".d-icon-times"))); + }, + }); - componentTest("content with html", { - template: TEMPLATE, + componentTest("content with html", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + }, - async test(assert) { - await toggle(); - assert.equal(rowById(4).innerHTML.trim(), "baz"); - }, - }); + async test(assert) { + await toggle(); + assert.equal(rowById(4).innerHTML.trim(), "baz"); + }, + }); - componentTest("separator", { - template: TEMPLATE, + componentTest("separator", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + }, - async test(assert) { - await toggle(); - assert.ok( - queryAll( - "#my-dropdown .widget-dropdown-item:nth-child(3)" - )[0].classList.contains("separator") - ); - }, - }); + async test(assert) { + await toggle(); + assert.ok( + queryAll( + "#my-dropdown .widget-dropdown-item:nth-child(3)" + )[0].classList.contains("separator") + ); + }, + }); - componentTest("hides widget if no content", { - template: TEMPLATE, + componentTest("hides widget if no content", { + template: TEMPLATE, - beforeEach() { - this.setProperties({ content: null, label: "foo" }); - }, + beforeEach() { + this.setProperties({ content: null, label: "foo" }); + }, - test(assert) { - assert.notOk(exists("#my-dropdown .widget-dropdown-header")); - assert.notOk(exists("#my-dropdown .widget-dropdown-body")); - }, - }); + test(assert) { + assert.notOk(exists("#my-dropdown .widget-dropdown-header")); + assert.notOk(exists("#my-dropdown .widget-dropdown-body")); + }, + }); - componentTest("headerClass option", { - template: TEMPLATE, + componentTest("headerClass option", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - this.set("options", { headerClass: "btn-small and-text" }); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + this.set("options", { headerClass: "btn-small and-text" }); + }, - test(assert) { - assert.ok(header().classList.contains("widget-dropdown-header")); - assert.ok(header().classList.contains("btn-small")); - assert.ok(header().classList.contains("and-text")); - }, - }); + test(assert) { + assert.ok(header().classList.contains("widget-dropdown-header")); + assert.ok(header().classList.contains("btn-small")); + assert.ok(header().classList.contains("and-text")); + }, + }); - componentTest("bodyClass option", { - template: TEMPLATE, + componentTest("bodyClass option", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - this.set("options", { bodyClass: "gigantic and-yet-small" }); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + this.set("options", { bodyClass: "gigantic and-yet-small" }); + }, - async test(assert) { - await toggle(); - assert.ok(body().classList.contains("widget-dropdown-body")); - assert.ok(body().classList.contains("gigantic")); - assert.ok(body().classList.contains("and-yet-small")); - }, - }); + async test(assert) { + await toggle(); + assert.ok(body().classList.contains("widget-dropdown-body")); + assert.ok(body().classList.contains("gigantic")); + assert.ok(body().classList.contains("and-yet-small")); + }, + }); - componentTest("caret option", { - template: TEMPLATE, + componentTest("caret option", { + template: TEMPLATE, - beforeEach() { - this.setProperties(DEFAULT_CONTENT); - this.set("options", { caret: true }); - }, + beforeEach() { + this.setProperties(DEFAULT_CONTENT); + this.set("options", { caret: true }); + }, - test(assert) { - assert.ok( - exists("#my-dropdown .widget-dropdown-header .d-icon-caret-down") - ); - }, - }); -}); + test(assert) { + assert.ok( + exists("#my-dropdown .widget-dropdown-header .d-icon-caret-down") + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/setup-tests.js b/app/assets/javascripts/discourse/tests/setup-tests.js index 39f97ab8b7..10924acedc 100644 --- a/app/assets/javascripts/discourse/tests/setup-tests.js +++ b/app/assets/javascripts/discourse/tests/setup-tests.js @@ -33,9 +33,9 @@ const Modal = Plugin.Constructor; function AcceptanceModal(option, _relatedTarget) { return this.each(function () { - var $this = $(this); - var data = $this.data("bs.modal"); - var options = $.extend( + let $this = $(this); + let data = $this.data("bs.modal"); + let options = $.extend( {}, Modal.DEFAULTS, $this.data(), @@ -202,8 +202,8 @@ export default function setupTests(app, container) { // Load ES6 tests function getUrlParameter(name) { name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); - var results = regex.exec(location.search); + let regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); + let results = regex.exec(location.search); return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); diff --git a/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js b/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js index ca8a6b1856..e67342fbb4 100644 --- a/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js +++ b/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js @@ -6,6 +6,7 @@ import { import KeyboardShortcutInitializer from "discourse/initializers/keyboard-shortcuts"; import { REMINDER_TYPES } from "discourse/lib/bookmark"; import User from "discourse/models/user"; +import { getApplication } from "@ember/test-helpers"; import sinon from "sinon"; import { test } from "qunit"; @@ -18,7 +19,7 @@ function mockMomentTz(dateString) { discourseModule("Unit | Controller | bookmark", function (hooks) { hooks.beforeEach(function () { logIn(); - KeyboardShortcutInitializer.initialize(this.container); + KeyboardShortcutInitializer.initialize(getApplication()); BookmarkController = this.owner.lookup("controller:bookmark"); BookmarkController.setProperties({ diff --git a/app/assets/javascripts/discourse/tests/unit/ember/resolver-test.js b/app/assets/javascripts/discourse/tests/unit/ember/resolver-test.js index decaab7f4a..155af39015 100644 --- a/app/assets/javascripts/discourse/tests/unit/ember/resolver-test.js +++ b/app/assets/javascripts/discourse/tests/unit/ember/resolver-test.js @@ -5,8 +5,8 @@ let originalTemplates; let resolver; function lookupTemplate(assert, name, expectedTemplate, message) { - var parseName = resolver.parseName(name); - var result = resolver.resolveTemplate(parseName); + let parseName = resolver.parseName(name); + let result = resolver.resolveTemplate(parseName); assert.equal(result, expectedTemplate, message); } diff --git a/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js b/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js index de2f56eed3..14b0b66d81 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js @@ -154,7 +154,7 @@ module("Unit | Utility | click-track", function (hooks) { assert.notOk(track(generateClickEventOn("a"))); - var $link = fixture("a").first(); + let $link = fixture("a").first(); assert.ok($link.hasClass("no-href")); assert.equal($link.data("href"), "http://www.google.com/"); assert.blank($link.attr("href")); @@ -178,7 +178,7 @@ module("Unit | Utility | click-track", function (hooks) { function badgeClickCount(assert, id, expected) { track(generateClickEventOn("#" + id)); - var $badge = $("span.badge", fixture("#" + id).first()); + let $badge = $("span.badge", fixture("#" + id).first()); assert.equal(parseInt($badge.html(), 10), expected); } @@ -200,7 +200,7 @@ module("Unit | Utility | click-track", function (hooks) { function testOpenInANewTab(description, clickEventModifier) { test(description, async function (assert) { - var clickEvent = generateClickEventOn("a"); + let clickEvent = generateClickEventOn("a"); clickEventModifier(clickEvent); assert.ok(track(clickEvent)); assert.notOk(clickEvent.defaultPrevented); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/computed-test.js b/app/assets/javascripts/discourse/tests/unit/lib/computed-test.js index 216e42cd63..e8d7cd3db9 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/computed-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/computed-test.js @@ -45,7 +45,7 @@ discourseModule("Unit | Utility | computed", function (hooks) { }); test("propertyEqual", function (assert) { - var t = EmberObject.extend({ + let t = EmberObject.extend({ same: propertyEqual("cookies", "biscuits"), }).create({ cookies: 10, @@ -58,7 +58,7 @@ discourseModule("Unit | Utility | computed", function (hooks) { }); test("propertyNotEqual", function (assert) { - var t = EmberObject.extend({ + let t = EmberObject.extend({ diff: propertyNotEqual("cookies", "biscuits"), }).create({ cookies: 10, @@ -71,7 +71,7 @@ discourseModule("Unit | Utility | computed", function (hooks) { }); test("fmt", function (assert) { - var t = EmberObject.extend({ + let t = EmberObject.extend({ exclaimyUsername: fmt("username", "!!! %@ !!!"), multiple: fmt("username", "mood", "%@ is %@"), }).create({ @@ -105,7 +105,7 @@ discourseModule("Unit | Utility | computed", function (hooks) { }); test("i18n", function (assert) { - var t = EmberObject.extend({ + let t = EmberObject.extend({ exclaimyUsername: i18n("username", "!!! %@ !!!"), multiple: i18n("username", "mood", "%@ is %@"), }).create({ @@ -139,7 +139,7 @@ discourseModule("Unit | Utility | computed", function (hooks) { }); test("url", function (assert) { - var t, testClass; + let t, testClass; testClass = EmberObject.extend({ userUrl: url("username", "/u/%@"), diff --git a/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js b/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js index fa7faff0fc..71cc9bc95a 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js @@ -159,7 +159,7 @@ discourseModule("Unit | Utility | formatter", function (hooks) { assert.equal(formatDays(-500), shortDateYear(-500)); assert.equal(formatDays(-365 * 2 - 1), shortDateYear(-365 * 2 - 1)); // one leap year - var originalValue = this.siteSettings.relative_date_duration; + let originalValue = this.siteSettings.relative_date_duration; this.siteSettings.relative_date_duration = 7; assert.equal(formatDays(7), "7d"); assert.equal(formatDays(8), shortDate(8)); @@ -203,9 +203,9 @@ discourseModule("Unit | Utility | formatter", function (hooks) { }); test("autoUpdatingRelativeAge", function (assert) { - var d = moment().subtract(1, "day").toDate(); + let d = moment().subtract(1, "day").toDate(); - var $elem = $(autoUpdatingRelativeAge(d)); + let $elem = $(autoUpdatingRelativeAge(d)); assert.equal($elem.data("format"), "tiny"); assert.equal($elem.data("time"), d.getTime()); assert.equal($elem.attr("title"), undefined); @@ -233,8 +233,8 @@ discourseModule("Unit | Utility | formatter", function (hooks) { }); test("updateRelativeAge", function (assert) { - var d = new Date(); - var $elem = $(autoUpdatingRelativeAge(d)); + let d = new Date(); + let $elem = $(autoUpdatingRelativeAge(d)); $elem.data("time", d.getTime() - 2 * 60 * 1000); updateRelativeAge($elem); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js b/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js index 82d86b838a..626de26679 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js @@ -60,6 +60,12 @@ module("Unit | Utility | get-url", function () { "relative url has subfolder" ); + assert.equal( + getURL("/u/forumadmin"), + "/forum/u/forumadmin", + "relative url has subfolder even if username contains subfolder" + ); + assert.equal( getURL(""), "/forum", diff --git a/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js b/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js index 25b6121bda..02be685e6b 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js @@ -194,7 +194,7 @@ discourseModule("Unit | Utility | uploads", function () { test("isImage", function (assert) { ["png", "webp", "jpg", "jpeg", "gif", "ico"].forEach((extension) => { - var image = "image." + extension; + let image = "image." + extension; assert.ok(isImage(image), image + " is recognized as an image"); assert.ok( isImage("http://foo.bar/path/to/" + image), diff --git a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js index e76b27877d..6739b729a7 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js @@ -71,7 +71,7 @@ discourseModule("Unit | Utilities", function () { }); test("avatarUrl", function (assert) { - var rawSize = getRawSize; + let rawSize = getRawSize; assert.blank(avatarUrl("", "tiny"), "no template returns blank"); assert.equal( avatarUrl("/fake/template/{size}.png", "tiny"), @@ -85,7 +85,7 @@ discourseModule("Unit | Utilities", function () { ); }); - var setDevicePixelRatio = function (value) { + let setDevicePixelRatio = function (value) { if (Object.defineProperty && !window.hasOwnProperty("devicePixelRatio")) { Object.defineProperty(window, "devicePixelRatio", { value: 2 }); } else { @@ -94,10 +94,10 @@ discourseModule("Unit | Utilities", function () { }; test("avatarImg", function (assert) { - var oldRatio = window.devicePixelRatio; + let oldRatio = window.devicePixelRatio; setDevicePixelRatio(2); - var avatarTemplate = "/path/to/avatar/{size}.png"; + let avatarTemplate = "/path/to/avatar/{size}.png"; assert.equal( avatarImg({ avatarTemplate: avatarTemplate, size: "tiny" }), "", @@ -164,7 +164,7 @@ discourseModule("Unit | Utilities", function () { }); test("caretRowCol", function (assert) { - var textarea = document.createElement("textarea"); + let textarea = document.createElement("textarea"); const content = document.createTextNode("01234\n56789\n012345"); textarea.appendChild(content); document.body.appendChild(textarea); diff --git a/app/assets/javascripts/discourse/tests/unit/localization-test.js b/app/assets/javascripts/discourse/tests/unit/localization-test.js index 4939746aed..f2bf2f3c0f 100644 --- a/app/assets/javascripts/discourse/tests/unit/localization-test.js +++ b/app/assets/javascripts/discourse/tests/unit/localization-test.js @@ -1,6 +1,7 @@ import { module, test } from "qunit"; import I18n from "I18n"; import LocalizationInitializer from "discourse/initializers/localization"; +import { getApplication } from "@ember/test-helpers"; module("initializer:localization", { _locale: I18n.locale, @@ -42,7 +43,7 @@ test("translation overrides", function (assert) { "js.composer.reply": "WAT", "js.topic.reply.help": "foobar", }; - LocalizationInitializer.initialize(this.registry); + LocalizationInitializer.initialize(getApplication()); assert.equal( I18n.t("composer.reply"), @@ -61,7 +62,7 @@ test("skip translation override if parent node is not an object", function (asse "js.composer.reply": "WAT", "js.composer.reply.help": "foobar", }; - LocalizationInitializer.initialize(this.registry); + LocalizationInitializer.initialize(getApplication()); assert.equal(I18n.t("composer.reply.help"), "[fr.composer.reply.help]"); }); diff --git a/app/assets/javascripts/discourse/tests/unit/mixins/singleton-test.js b/app/assets/javascripts/discourse/tests/unit/mixins/singleton-test.js index b142e52042..0a2e29d0e5 100644 --- a/app/assets/javascripts/discourse/tests/unit/mixins/singleton-test.js +++ b/app/assets/javascripts/discourse/tests/unit/mixins/singleton-test.js @@ -4,10 +4,10 @@ import Singleton from "discourse/mixins/singleton"; module("Unit | Mixin | singleton", function () { test("current", function (assert) { - var DummyModel = EmberObject.extend({}); + let DummyModel = EmberObject.extend({}); DummyModel.reopenClass(Singleton); - var current = DummyModel.current(); + let current = DummyModel.current(); assert.present(current, "current returns the current instance"); assert.equal( current, @@ -22,9 +22,9 @@ module("Unit | Mixin | singleton", function () { }); test("currentProp reading", function (assert) { - var DummyModel = EmberObject.extend({}); + let DummyModel = EmberObject.extend({}); DummyModel.reopenClass(Singleton); - var current = DummyModel.current(); + let current = DummyModel.current(); assert.blank( DummyModel.currentProp("evil"), @@ -39,14 +39,14 @@ module("Unit | Mixin | singleton", function () { }); test("currentProp writing", function (assert) { - var DummyModel = EmberObject.extend({}); + let DummyModel = EmberObject.extend({}); DummyModel.reopenClass(Singleton); assert.blank( DummyModel.currentProp("adventure"), "by default attributes are blank" ); - var result = DummyModel.currentProp("adventure", "time"); + let result = DummyModel.currentProp("adventure", "time"); assert.equal(result, "time", "it returns the new value"); assert.equal( DummyModel.currentProp("adventure"), @@ -70,7 +70,7 @@ module("Unit | Mixin | singleton", function () { }); test("createCurrent", function (assert) { - var Shoe = EmberObject.extend({}); + let Shoe = EmberObject.extend({}); Shoe.reopenClass(Singleton, { createCurrent: function () { return Shoe.create({ toes: 5 }); @@ -85,7 +85,7 @@ module("Unit | Mixin | singleton", function () { }); test("createCurrent that returns null", function (assert) { - var Missing = EmberObject.extend({}); + let Missing = EmberObject.extend({}); Missing.reopenClass(Singleton, { createCurrent: function () { return null; diff --git a/app/assets/javascripts/discourse/tests/unit/models/post-test.js b/app/assets/javascripts/discourse/tests/unit/models/post-test.js index b85d5762b7..82b5e52805 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/post-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/post-test.js @@ -18,13 +18,13 @@ function buildPost(args) { module("Unit | Model | post", function () { test("defaults", function (assert) { - var post = Post.create({ id: 1 }); + let post = Post.create({ id: 1 }); assert.blank(post.get("deleted_at"), "it has no deleted_at by default"); assert.blank(post.get("deleted_by"), "there is no deleted_by by default"); }); test("new_user", function (assert) { - var post = Post.create({ trust_level: 0 }); + let post = Post.create({ trust_level: 0 }); assert.ok(post.get("new_user"), "post is from a new user"); post.set("trust_level", 1); @@ -32,7 +32,7 @@ module("Unit | Model | post", function () { }); test("firstPost", function (assert) { - var post = Post.create({ post_number: 1 }); + let post = Post.create({ post_number: 1 }); assert.ok(post.get("firstPost"), "it's the first post"); post.set("post_number", 10); @@ -40,7 +40,7 @@ module("Unit | Model | post", function () { }); test("updateFromPost", function (assert) { - var post = Post.create({ + let post = Post.create({ post_number: 1, raw: "hello world", }); diff --git a/app/assets/javascripts/discourse/tests/unit/models/rest-model-test.js b/app/assets/javascripts/discourse/tests/unit/models/rest-model-test.js index 8fe391217d..b549826f1a 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/rest-model-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/rest-model-test.js @@ -15,7 +15,7 @@ module("Unit | Model | rest-model", function () { }, }); - var g = Grape.create({ store, percent: 0.4 }); + let g = Grape.create({ store, percent: 0.4 }); assert.equal(g.get("inverse"), 0.6, "it runs `munge` on `create`"); }); diff --git a/app/assets/javascripts/discourse/tests/unit/models/topic-details-test.js b/app/assets/javascripts/discourse/tests/unit/models/topic-details-test.js index 8cbe7b06b5..4e11fe0324 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/topic-details-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/topic-details-test.js @@ -9,13 +9,13 @@ function buildDetails(id) { module("Unit | Model | topic-details", function () { test("defaults", function (assert) { - var details = buildDetails(1234); + let details = buildDetails(1234); assert.present(details, "the details are present by default"); assert.ok(!details.get("loaded"), "details are not loaded by default"); }); test("updateFromJson", function (assert) { - var details = buildDetails(1234); + let details = buildDetails(1234); details.updateFromJson({ allowed_users: [{ username: "eviltrout" }], diff --git a/app/assets/javascripts/discourse/tests/unit/models/user-action-test.js b/app/assets/javascripts/discourse/tests/unit/models/user-action-test.js index 9b6518c3de..50cd70e739 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/user-action-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/user-action-test.js @@ -3,7 +3,7 @@ import UserAction from "discourse/models/user-action"; module("Unit | Model | user-action", function () { test("collapsing likes", function (assert) { - var actions = UserAction.collapseStream([ + let actions = UserAction.collapseStream([ UserAction.create({ action_type: UserAction.TYPES.likes_given, topic_id: 1, diff --git a/app/assets/javascripts/discourse/tests/unit/models/user-stream-test.js b/app/assets/javascripts/discourse/tests/unit/models/user-stream-test.js index 9fdeb815a6..8ba20c9cc6 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/user-stream-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/user-stream-test.js @@ -4,8 +4,8 @@ import UserAction from "discourse/models/user-action"; module("Unit | Model | user-stream", function () { test("basics", function (assert) { - var user = User.create({ id: 1, username: "eviltrout" }); - var stream = user.get("stream"); + let user = User.create({ id: 1, username: "eviltrout" }); + let stream = user.get("stream"); assert.present(stream, "a user has a stream by default"); assert.equal( stream.get("user"), @@ -25,8 +25,8 @@ module("Unit | Model | user-stream", function () { }); test("filterParam", function (assert) { - var user = User.create({ id: 1, username: "eviltrout" }); - var stream = user.get("stream"); + let user = User.create({ id: 1, username: "eviltrout" }); + let stream = user.get("stream"); // defaults to posts/topics assert.equal(stream.get("filterParam"), "4,5"); diff --git a/app/assets/javascripts/discourse/tests/unit/models/user-test.js b/app/assets/javascripts/discourse/tests/unit/models/user-test.js index d17725a6f8..0b95967b12 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/user-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/user-test.js @@ -7,7 +7,7 @@ import sinon from "sinon"; module("Unit | Model | user", function () { test("staff", function (assert) { - var user = User.create({ id: 1, username: "eviltrout" }); + let user = User.create({ id: 1, username: "eviltrout" }); assert.ok(!user.get("staff"), "user is not staff"); @@ -19,7 +19,7 @@ module("Unit | Model | user", function () { }); test("searchContext", function (assert) { - var user = User.create({ id: 1, username: "EvilTrout" }); + let user = User.create({ id: 1, username: "EvilTrout" }); assert.deepEqual( user.get("searchContext"), @@ -29,7 +29,7 @@ module("Unit | Model | user", function () { }); test("isAllowedToUploadAFile", function (assert) { - var user = User.create({ trust_level: 0, admin: true }); + let user = User.create({ trust_level: 0, admin: true }); assert.ok( user.isAllowedToUploadAFile("image"), "admin can always upload a file" diff --git a/app/assets/javascripts/embed-application.js b/app/assets/javascripts/embed-application.js index 4ba6a082fa..f71ac900ef 100644 --- a/app/assets/javascripts/embed-application.js +++ b/app/assets/javascripts/embed-application.js @@ -9,11 +9,11 @@ } function clickPostLink(e) { - var postId = e.target.getAttribute("data-link-to-post"); + let postId = e.target.getAttribute("data-link-to-post"); if (postId) { - var postElement = document.getElementById("post-" + postId); + let postElement = document.getElementById("post-" + postId); if (postElement) { - var rect = postElement.getBoundingClientRect(); + let rect = postElement.getBoundingClientRect(); if (rect && rect.top) { postUp({ type: "discourse-scroll", top: rect.top }); e.preventDefault(); @@ -25,9 +25,9 @@ window.onload = function () { // get state info from data attribute - var embedState = document.querySelector("[data-embed-state]"); - var state = "unknown"; - var embedId = null; + let embedState = document.querySelector("[data-embed-state]"); + let state = "unknown"; + let embedId = null; if (embedState) { state = embedState.getAttribute("data-embed-state"); embedId = embedState.getAttribute("data-embed-id"); @@ -41,7 +41,7 @@ embedId, }); - var postLinks = document.querySelectorAll("a[data-link-to-post]"), + let postLinks = document.querySelectorAll("a[data-link-to-post]"), i; for (i = 0; i < postLinks.length; i++) { @@ -49,15 +49,15 @@ } // Make sure all links in the iframe point to _blank - var cookedLinks = document.querySelectorAll(".cooked a"); + let cookedLinks = document.querySelectorAll(".cooked a"); for (i = 0; i < cookedLinks.length; i++) { cookedLinks[i].target = "_blank"; } // Adjust all names - var names = document.querySelectorAll(".username a"); + let names = document.querySelectorAll(".username a"); for (i = 0; i < names.length; i++) { - var username = names[i].innerHTML; + let username = names[i].innerHTML; if (username) { /* global BreakString */ names[i].innerHTML = new BreakString(username).break(); diff --git a/app/assets/javascripts/handlebars-shim.js b/app/assets/javascripts/handlebars-shim.js index fb191149a1..9d8403d189 100644 --- a/app/assets/javascripts/handlebars-shim.js +++ b/app/assets/javascripts/handlebars-shim.js @@ -5,6 +5,10 @@ if (typeof define !== "undefined") { if (typeof Handlebars !== "undefined") { // eslint-disable-next-line __exports__.default = Handlebars; + __exports__.compile = function () { + // eslint-disable-next-line + return Handlebars.compile.apply(this, arguments); + }; } }); diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js index 4160d7986d..c9d2c714a4 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js @@ -191,7 +191,7 @@ function findInlineCloseTag(state, openTag, start, max) { } function applyBBCode(state, startLine, endLine, silent, md) { - var nextLine, + let nextLine, oldParent, oldLineMax, rule, diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js index cd24628689..069a88c2f3 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js @@ -15,7 +15,7 @@ export default null; // based off https://github.com/markdown-it/markdown-it-emoji/blob/master/dist/markdown-it-emoji.js // export function textReplace(state, callback, skipAllLinks) { - var i, + let i, j, l, tokens, diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html-img.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html-img.js index 596c45ad7f..e876499a25 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html-img.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html-img.js @@ -5,7 +5,7 @@ const REGEX = /^\s*$/i; function rule(state, startLine, endLine) { - var nextLine, + let nextLine, token, lineText, pos = state.bMarks[startLine] + state.tShift[startLine], diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js index 1de6a74a36..5781c9df20 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js @@ -5,7 +5,7 @@ // this is a freedom patch, so careful, may break on updates function newline(state, silent) { - var token, + let token, pmax, max, pos = state.pos; diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js index a44a0214d7..79c5166769 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js @@ -4,7 +4,7 @@ // link starts with a space, so we can bypass a onebox // this is a freedom patch, so careful, may break on updates function paragraph(state, startLine /*, endLine*/) { - var content, + let content, terminate, i, l, diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js index c7e0dc9ca7..45dc70ebcd 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js @@ -113,7 +113,7 @@ const rule = { if (offTopicQuote) { const topicInfo = options.getTopicInfo(topicId); if (topicInfo) { - var href = topicInfo.href; + let href = topicInfo.href; if (postNumber > 0) { href += "/" + postNumber; } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/resize-controls.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/resize-controls.js index 63bf3f7059..8b34244611 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/resize-controls.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/resize-controls.js @@ -90,8 +90,8 @@ export function setup(helper) { const imageIndex = token.attrIndex("index-image"); if (scaleIndex !== -1) { - var selectedScale = token.attrs[scaleIndex][1]; - var index = token.attrs[imageIndex][1]; + let selectedScale = token.attrs[scaleIndex][1]; + let index = token.attrs[imageIndex][1]; let result = ""; result += oldRule(tokens, idx, options, env, slf); diff --git a/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs index 5213bceac9..59504133b9 100644 --- a/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs +++ b/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs @@ -11,6 +11,7 @@ class="btn-clear" icon="times" action=selectKit.onClearSelection + ariaLabel="clear_input" }} {{/if}} diff --git a/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs b/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs index da45408eac..c826cea9a7 100644 --- a/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs +++ b/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs @@ -1,5 +1,5 @@ {{#if selectKit.options.showFullTitle}} -
    +
    {{#if item.icon}} {{d-icon item.icon}} {{/if}} @@ -17,6 +17,7 @@ icon="times" action=selectKit.deselect actionParam=item + ariaLabel="clear_input" }} {{/if}} {{/if}} diff --git a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js index 4c00de2810..7f935e4fb4 100644 --- a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js +++ b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js @@ -3,7 +3,7 @@ import { module, test } from "qunit"; import { run } from "@ember/runloop"; import startApp from "wizard/test/helpers/start-app"; -var wizard; +let wizard; module("Acceptance: wizard", { beforeEach() { wizard = startApp(); diff --git a/app/assets/javascripts/wizard/test/test_helper.js b/app/assets/javascripts/wizard/test/test_helper.js index 05d0eb3c9f..3aec94db05 100644 --- a/app/assets/javascripts/wizard/test/test_helper.js +++ b/app/assets/javascripts/wizard/test/test_helper.js @@ -25,7 +25,7 @@ //= require test-shims // Trick JSHint into allow document.write -var d = document; +let d = document; d.write( '
    ' ); @@ -40,14 +40,14 @@ if (window.Logster) { } Ember.Test.adapter = window.QUnitAdapter.create(); -var createPretendServer = requirejs( +let createPretendServer = requirejs( "wizard/test/wizard-pretender", null, null, false ).default; -var server; +let server; QUnit.testStart(function () { server = createPretendServer(); }); @@ -56,8 +56,8 @@ QUnit.testDone(function () { server.shutdown(); }); -var _testApp = requirejs("wizard/test/helpers/start-app").default(); -var _buildResolver = requirejs("discourse-common/resolver").buildResolver; +let _testApp = requirejs("wizard/test/helpers/start-app").default(); +let _buildResolver = requirejs("discourse-common/resolver").buildResolver; window.setResolver(_buildResolver("wizard").create({ namespace: _testApp })); Object.keys(requirejs.entries).forEach(function (entry) { diff --git a/app/assets/stylesheets/common/admin/dashboard.scss b/app/assets/stylesheets/common/admin/dashboard.scss index 8d38420b7b..0e553c4a9a 100644 --- a/app/assets/stylesheets/common/admin/dashboard.scss +++ b/app/assets/stylesheets/common/admin/dashboard.scss @@ -612,3 +612,42 @@ font-size: $font-up-3; } } + +.dashboard-new-features { + .section-body { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 1.5em; + } + + .section-footer { + margin: 1.5em; + display: flex; + justify-content: flex-end; + align-items: center; + .btn { + margin-left: 1em; + } + } +} + +.admin-new-feature-item { + display: flex; + align-items: flex-start; + + .new-feature-emoji { + font-size: 3.5em; + padding-right: 0.5em; + padding-left: 0.5em; + } + + .new-feature-content { + padding-right: 0.5em; + align-self: center; + .header { + font-size: $font-up-1; + font-weight: bold; + margin-bottom: 0.5em; + } + } +} diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index d362f9876b..c38b7e17db 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -34,18 +34,6 @@ max-height: 40vh; } } - - @include breakpoint(medium) { - // hide button labels to save space - .btn { - .d-button-label { - display: none; - } - .d-icon { - margin: 0; - } - } - } } .navigation-container { @@ -81,6 +69,14 @@ > * { white-space: nowrap; } + .select-kit button { + height: 100%; // ensures nested select-kit button matches height of button siblings + } + .btn { + // need to reduce vertical padding for consistent height + padding-top: 0.3em; + padding-bottom: 0.3em; + } } .show-more { @@ -295,15 +291,23 @@ } .heatmap-high, -.heatmap-high a { +.heatmap-high a, +.heatmap-high .d-icon, +.heatmap-high .d-button-label { color: #fe7a15 !important; } + .heatmap-med, -.heatmap-med a { +.heatmap-med a, +.heatmap-med .d-icon, +.heatmap-med .d-button-label { color: #cf7721 !important; } + .heatmap-low, -.heatmap-low a { +.heatmap-low a, +.heatmap-low .d-icon, +.heatmap-low .d-button-label { color: #9b764f !important; } diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 78e721198c..379302565a 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -122,6 +122,7 @@ } .action-title { + line-height: normal; @include ellipsis; } diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 9bdd3be77a..ebaeaa087d 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -859,3 +859,24 @@ table { z-index: z("max"); color: var(--secondary); } + +.controls { + .grouped-control { + display: flex; + flex-direction: column; + + .grouped-control-label { + padding: 0.25em 0; + } + + .grouped-control-field { + flex: 1 0 auto; + display: flex; + padding-bottom: 0.25em; + + label { + margin: 0; + } + } + } +} diff --git a/app/assets/stylesheets/common/base/edit-category.scss b/app/assets/stylesheets/common/base/edit-category.scss index dabcc81172..b0145b246e 100644 --- a/app/assets/stylesheets/common/base/edit-category.scss +++ b/app/assets/stylesheets/common/base/edit-category.scss @@ -7,7 +7,7 @@ div.edit-category { grid-template-rows: auto auto auto; grid-row-gap: 1em; grid-column-gap: 1.5em; - grid-template-areas: "header header" "sidebar content" "sidebar footer"; + grid-template-areas: "header header" "sidebar content" "sidebar warning" "sidebar footer"; .edit-category-title { grid-area: header; @@ -141,37 +141,16 @@ div.edit-category { } } + .edit-category-delete-warning { + grid-area: warning; + } + .edit-category-footer { grid-area: footer; display: flex; justify-content: space-between; align-self: start; padding: 0 1.5em 2em 0; - - .disable-info { - position: relative; - .cannot-delete-reason { - position: absolute; - bottom: 125%; - right: 0px; - width: 250px; - background: var(--primary); - color: var(--secondary); - text-align: center; - border-radius: 2px; - padding: 12px 8px; - - &::after { - top: 100%; - left: 57%; - border: solid transparent; - content: " "; - position: absolute; - border-top-color: var(--primary); - border-width: 8px; - } - } - } } } diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 9d93eb26f8..2fde6944ae 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -380,76 +380,73 @@ div.menu-links-header { display: flex; width: 100%; z-index: 2; + justify-content: space-between; - li { + .glyphs { display: inline-flex; align-items: center; - flex-wrap: wrap; + flex-wrap: nowrap; + width: 100%; + justify-content: space-between; + padding: 0; - &.glyphs { - flex-wrap: nowrap; - width: 100%; - justify-content: space-between; - padding: 0; - - a { - display: flex; - flex: 1 1 auto; - padding: 0.65em 0.25em 0.75em; - justify-content: center; - } - } - - a, button { - // This is to make sure active and inactive tab icons have the same - // size. `box-sizing` does not work and I have no idea why. - border: 1px solid transparent; - &:not(.active):hover { - border-bottom: 0; - margin-top: -1px; - } + display: flex; + flex: 1 1 auto; + padding: 0.65em 0.25em 0.75em; + justify-content: center; + } + } + + button { + // This is to make sure active and inactive tab icons have the same + // size. `box-sizing` does not work and I have no idea why. + border: 1px solid transparent; + &:not(.active):hover { + border-bottom: 0; + margin-top: -1px; + } + } + + button.active { + border: 1px solid var(--primary-low); + border-bottom: 1px solid var(--secondary); + position: relative; + + .d-icon { + color: var(--primary-high); } - a.active { - border: 1px solid var(--primary-low); - border-bottom: 1px solid var(--secondary); - position: relative; - - .d-icon { - color: var(--primary-high); - } - - &:focus, - &:hover { - background-color: inherit; - } + &:focus, + &:hover { + background-color: inherit; } } } - a:hover, - a:focus { + + button:hover, + button:focus { background-color: var(--highlight-medium); outline: none; } - a { + button { padding: 0.3em 0.5em; } - li { + .glyphs { display: table-cell; width: auto; text-align: center; } - li:first-child { + .glyphs:first-child { text-align: left; } - li:last-child { + .glyphs:last-child { text-align: right; } .fa, - a { + button { color: var(--primary-med-or-secondary-med); } } diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 77c2bdccd9..a6ef072f02 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -162,14 +162,22 @@ } .modal-footer { + display: flex; + flex-wrap: wrap; + align-items: center; padding: 14px 15px 10px; border-top: 1px solid var(--primary-low); .btn { - margin: 0 5px 5px 0; + margin: 0 0.3em 0 0; &[href] { min-height: unset; } } + + .btn, + a { + margin-bottom: 0.3em; + } } .modal { diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 750d1627f2..b8c0a81bba 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -803,3 +803,9 @@ aside.onebox.stackexchange .onebox-body { } } } + +aside.onebox.preview-error .site-icon { + width: 16px; + height: 16px; + margin-right: 0.5em; +} diff --git a/app/assets/stylesheets/common/base/popup-menu.scss b/app/assets/stylesheets/common/base/popup-menu.scss index 56f2a0bc2c..a69e7ee920 100644 --- a/app/assets/stylesheets/common/base/popup-menu.scss +++ b/app/assets/stylesheets/common/base/popup-menu.scss @@ -19,12 +19,12 @@ } .btn { - display: flex; - text-align: left; + justify-content: left; background: none; width: 100%; padding: 0.75em; border-radius: 0; + margin: 0; .d-icon { color: var(--primary-medium); diff --git a/app/assets/stylesheets/common/base/reviewables.scss b/app/assets/stylesheets/common/base/reviewables.scss index 49c8d980f3..7fbbb1a594 100644 --- a/app/assets/stylesheets/common/base/reviewables.scss +++ b/app/assets/stylesheets/common/base/reviewables.scss @@ -188,11 +188,6 @@ width: auto; } - .refresh { - height: 1em; - display: flex; - } - .score-filter { width: 5em; } diff --git a/app/assets/stylesheets/common/base/rtl.scss b/app/assets/stylesheets/common/base/rtl.scss index de3ac13d76..0995f19ca6 100644 --- a/app/assets/stylesheets/common/base/rtl.scss +++ b/app/assets/stylesheets/common/base/rtl.scss @@ -120,3 +120,12 @@ } } } + +.rtl .ace_placeholder { + direction: rtl !important; + text-align: right !important; + + [dir="ltr"] { + text-align: left !important; + } +} diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index a6154bfc19..d4377f0331 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -11,7 +11,7 @@ .search-bar { display: flex; justify-content: space-between; - align-items: center; + align-items: stretch; margin-bottom: 1em; .search-query { @@ -128,6 +128,7 @@ } .search-advanced-title { + font-size: $font-up-1; background: var(--primary-low); padding: 0.358em 1em; @include breakpoint(medium) { diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 9083ca2a94..398da477bb 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -258,6 +258,9 @@ header .discourse-tag { margin-bottom: 10px; } } + .tag-group-controls { + display: flex; + } .group-tags-list .tag-chooser { width: 100%; } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 67eb33a49a..2ca3d905fd 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -655,6 +655,35 @@ blockquote { &.raw-email { cursor: pointer; } + &.edits { + .widget-button { + display: flex; + align-items: center; + + .d-button-label { + order: 0; + padding-right: 0.25em; + color: var(--primary-med-or-secondary-med); + transition: color 0.25s; + } + + .d-icon { + order: 1; + transition: color 0.25s; + color: var(--primary-med-or-secondary-med); + } + .discourse-no-touch & { + &:hover { + .d-button-label { + color: var(--primary-high); + } + .d-icon { + color: var(--primary-high); + } + } + } + } + } } pre { @@ -1049,31 +1078,21 @@ a.mention-group { } #topic-footer-buttons { - padding: 0.5em 0; + padding: 0.75em 0; .topic-footer-main-buttons { - margin: 0 0 -0.5em 0; display: flex; flex-wrap: wrap; - - > .btn { - margin: 0 0.5em 0.5em 0; - display: inline-flex; - align-items: center; - - .d-button-label { - display: flex; - flex: 1 0 auto; - align-items: center; - } + align-items: stretch; // aligns items by making them the same height + button { + margin-right: 0.54em; } .topic-admin-menu-button-container { display: inline-flex; - } - - .topic-admin-menu-button-container > span:not(:empty) { - margin: 0 0.5em 0.5em 0; + > span { + display: flex; // to make this button match siblings behavior, all of its parents need to be flex + } } } diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index dfe6c33825..2188b94246 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -217,7 +217,6 @@ .btn { margin-bottom: 10px; - line-height: $line-height-medium; } } diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index e00d5c2876..f97a14ff04 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -13,25 +13,24 @@ $hover-bg-color: var(--primary-medium), $hover-icon-color: var(--primary-low) ) { - display: inline-block; - margin: 0; - padding: 6px 12px; - min-height: 30px; - border: none; box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 0.53em 0.8em; + border: none; font-weight: normal; + font-size: $font-0; + line-height: normal; color: $text-color; background: $bg-color; - font-size: $font-0; - line-height: $line-height-small; - text-align: center; cursor: pointer; transition: all 0.25s; .d-icon { color: $icon-color; margin-right: 0.45em; transition: color 0.25s; - line-height: $line-height-medium; // Match button text line-height } &.no-text { .d-icon { diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 1f1b3f6580..7763081b4b 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -22,6 +22,7 @@ @extend .clearfix; > li { + display: flex; float: left; margin-right: 0.5em; diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 73ff05bf8d..e01d994c11 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -263,7 +263,7 @@ // shared styles for all font sizes .btn, .btn-default { - padding: 0 0.5em; + padding: 0.5em; } .d-editor-spacer { margin: 0 0.25em; diff --git a/app/assets/stylesheets/desktop/category-list.scss b/app/assets/stylesheets/desktop/category-list.scss index 3ed65ef0af..11443cd002 100644 --- a/app/assets/stylesheets/desktop/category-list.scss +++ b/app/assets/stylesheets/desktop/category-list.scss @@ -38,6 +38,7 @@ .value { font-size: $font-up-1; + font-weight: bold; } .unit { font-size: $font-down-1; diff --git a/app/assets/stylesheets/desktop/latest-topic-list.scss b/app/assets/stylesheets/desktop/latest-topic-list.scss index 33aeee78b5..a096f5ddae 100644 --- a/app/assets/stylesheets/desktop/latest-topic-list.scss +++ b/app/assets/stylesheets/desktop/latest-topic-list.scss @@ -11,6 +11,11 @@ .more-topics { margin-top: 1em; } + + .posts-map { + font-size: $font-up-1; + font-weight: bold; + } } .latest-topic-list-item { @@ -53,6 +58,6 @@ } .topic-last-activity a { - color: var(--primary-med-or-secondary-high); + color: var(--primary-low-mid-or-secondary-high); } } diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index aed20e9962..ddb0c6f36b 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -186,15 +186,11 @@ > ul { display: inline-flex; > li { - display: inline; - margin-left: 12px; - .d-button-label { - line-height: 1; - } + display: inline-flex; + margin-left: 0.75em; } a { - margin-bottom: 10px; width: auto; } } diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index a10d75c209..e78c827a70 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -237,9 +237,19 @@ a.reply-to-tab { color: var(--tertiary); } + .topic-footer-main-buttons { + display: flex; + align-items: stretch; + } + .topic-footer-mobile-dropdown { - margin: 0 0.5em 0.5em 0; + margin: 0 0.75em 0 0; width: 160px; + display: flex; + } + + .select-kit-header { + height: 100%; } .topic-notifications-button, diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index eb246d75e5..1b75618f88 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -22,4 +22,15 @@ class Admin::DashboardController < Admin::AdminController def problems render_json_dump(problems: AdminDashboardData.fetch_problems(check_force_https: request.ssl?)) end + + def new_features + data = { new_features: DiscourseUpdates.unseen_new_features(current_user.id) } + data.merge!(release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"]) + render json: data + end + + def mark_new_features_as_seen + DiscourseUpdates.mark_new_features_as_seen(current_user.id) + render json: success_json + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 53878b0396..c501ff1447 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -819,7 +819,7 @@ class ApplicationController < ActionController::Base params[:slug] = params[:slug].first if params[:slug].kind_of?(Array) params[:id] = params[:id].first if params[:id].kind_of?(Array) - @slug = (params[:slug].presence || params[:id].presence || "").tr('-', ' ') + @slug = (params[:slug].presence || params[:id].presence || "").to_s.tr('-', ' ') render_to_string status: opts[:status], layout: opts[:layout], formats: [:html], template: '/exceptions/not_found' end diff --git a/app/controllers/do_not_disturb_controller.rb b/app/controllers/do_not_disturb_controller.rb index 3f43246844..db24c7167c 100644 --- a/app/controllers/do_not_disturb_controller.rb +++ b/app/controllers/do_not_disturb_controller.rb @@ -25,9 +25,8 @@ class DoNotDisturbController < ApplicationController def destroy current_user.active_do_not_disturb_timings.destroy_all current_user.publish_do_not_disturb(ends_at: nil) - current_user.notifications.unprocessed.each do |notification| - NotificationEmailer.process_notification(notification, no_delay: true) - end + current_user.shelved_notifications.each(&:process) + current_user.shelved_notifications.destroy_all render json: success_json end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index fb4723c21f..0b754ef160 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -94,8 +94,7 @@ class InvitesController < ApplicationController guardian.ensure_can_invite_to_forum!(groups) group_ids = groups.map(&:id) - invite_exists = Invite.exists?(email: params[:email], invited_by_id: current_user.id) - if invite_exists && !guardian.can_send_multiple_invites?(current_user) + if Invite.exists?(email: params[:email]) return render json: failed_json, status: 422 end diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb index 35474d8152..c83e8800ce 100644 --- a/app/controllers/reviewables_controller.rb +++ b/app/controllers/reviewables_controller.rb @@ -189,7 +189,7 @@ class ReviewablesController < ApplicationController return render_json_error(error) end - args.merge!(reject_reason: params[:reject_reason], send_email: params[:send_email] == "true") if reviewable.type == 'ReviewableUser' + args.merge!(reject_reason: params[:reject_reason], send_email: params[:send_email] != "false") if reviewable.type == 'ReviewableUser' result = reviewable.perform(current_user, params[:action_id].to_sym, args) rescue Reviewable::InvalidAction => e diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2c4163e986..651fda8340 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1041,18 +1041,14 @@ class UsersController < ApplicationController def search_users term = params[:term].to_s.strip - topic_id = params[:topic_id] - topic_id = topic_id.to_i if topic_id - - category_id = params[:category_id].to_i if category_id.present? + topic_id = params[:topic_id].to_i if params[:topic_id].present? + category_id = params[:category_id].to_i if params[:category_id].present? topic_allowed_users = params[:topic_allowed_users] || false group_names = params[:groups] || [] group_names << params[:group] if params[:group] - if group_names.present? - @groups = Group.where(name: group_names) - end + @groups = Group.where(name: group_names) if group_names.present? options = { topic_allowed_users: topic_allowed_users, @@ -1060,13 +1056,8 @@ class UsersController < ApplicationController groups: @groups } - if topic_id - options[:topic_id] = topic_id - end - - if category_id - options[:category_id] = category_id - end + options[:topic_id] = topic_id if topic_id + options[:category_id] = category_id if category_id results = UserSearch.new(term, options).search @@ -1075,8 +1066,10 @@ class UsersController < ApplicationController to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template]) } + # blank term is only handy for in-topic search of users after @ + # we do not want group results ever if term is blank groups = - if current_user + if term.present? && current_user if params[:include_groups] == 'true' Group.visible_groups(current_user) elsif params[:include_mentionable_groups] == 'true' @@ -1086,10 +1079,6 @@ class UsersController < ApplicationController end end - # blank term is only handy for in-topic search of users after @ - # we do not want group results ever if term is blank - groups = nil if term.blank? - if groups groups = Group.search_groups(term, groups: groups) groups = groups.order('groups.name asc') diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0b994496dc..0f94822298 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -560,4 +560,16 @@ module ApplicationHelper end end end + + def authentication_data + return @authentication_data if defined?(@authentication_data) + + @authentication_data = begin + value = cookies[:authentication_data] + if value + cookies.delete(:authentication_data, path: Discourse.base_path("/")) + end + current_user ? nil : value + end + end end diff --git a/app/jobs/onceoff/fix_post_search_data_after_default_locale_rename.rb b/app/jobs/onceoff/fix_post_search_data_after_default_locale_rename.rb new file mode 100644 index 0000000000..76e0b10d24 --- /dev/null +++ b/app/jobs/onceoff/fix_post_search_data_after_default_locale_rename.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Jobs + class FixPostSearchDataAfterDefaultLocaleRename < ::Jobs::Onceoff + def execute_onceoff(args) + return if SearchIndexer::POST_INDEX_VERSION != 4 + + sql = <<~SQL + UPDATE post_search_data + SET locale = 'en' + WHERE post_id IN ( + SELECT post_id + FROM post_search_data + WHERE locale = 'en_US' + LIMIT 100000 + ) + SQL + + loop { break if DB.exec(sql) == 0 } + end + end +end diff --git a/app/jobs/scheduled/process_shelved_notifications.rb b/app/jobs/scheduled/process_shelved_notifications.rb index 38470a18d3..d734e51e81 100644 --- a/app/jobs/scheduled/process_shelved_notifications.rb +++ b/app/jobs/scheduled/process_shelved_notifications.rb @@ -6,22 +6,17 @@ module Jobs def execute(args) sql = <<~SQL - SELECT n.id FROM notifications AS n - INNER JOIN do_not_disturb_timings AS dndt ON n.user_id = dndt.user_id - WHERE n.processed = false + SELECT sn.id FROM shelved_notifications as sn + INNER JOIN notifications AS notification ON sn.notification_id = notification.id + INNER JOIN do_not_disturb_timings AS dndt ON notification.user_id = dndt.user_id AND dndt.ends_at <= :now SQL now = Time.zone.now - notification_ids = DB.query_single(sql, now: now) - - Notification.where(id: notification_ids).each do |notification| - begin - NotificationEmailer.process_notification(notification, no_delay: true) - rescue - Rails.logger.warn("Failed to process notification with ID #{notification.id}") - end - end + shelved_notification_ids = DB.query_single(sql, now: now) + shelved_notifications = ShelvedNotification.where(id: shelved_notification_ids) + shelved_notifications.each(&:process) + shelved_notifications.destroy_all DB.exec("DELETE FROM do_not_disturb_timings WHERE ends_at < :now", now: now) end diff --git a/app/jobs/scheduled/reindex_search.rb b/app/jobs/scheduled/reindex_search.rb index 3d6579c78f..26215cdc7a 100644 --- a/app/jobs/scheduled/reindex_search.rb +++ b/app/jobs/scheduled/reindex_search.rb @@ -1,201 +1,162 @@ # frozen_string_literal: true module Jobs - # if locale changes or search algorithm changes we may want to reindex stuff class ReindexSearch < ::Jobs::Scheduled every 2.hours - CLEANUP_GRACE_PERIOD = 1.day.ago - def execute(args) - @verbose = true if args && Hash === args && args[:verbose] + @verbose = args[:verbose] + @cleanup_grace_period = 1.day.ago - rebuild_problem_topics - rebuild_problem_posts - rebuild_problem_categories - rebuild_problem_users - rebuild_problem_tags - clean_post_search_data - clean_topic_search_data + rebuild_categories + rebuild_tags + rebuild_topics + rebuild_posts + rebuild_users - @verbose = nil + clean_topics + clean_posts end - def rebuild_problem_categories(limit: 500) + def rebuild_categories(limit: 500, indexer: SearchIndexer) category_ids = load_problem_category_ids(limit) - if @verbose - puts "rebuilding #{category_ids.length} categories" - end + puts "rebuilding #{category_ids.size} categories" if @verbose category_ids.each do |id| category = Category.find_by(id: id) - SearchIndexer.index(category, force: true) if category + indexer.index(category, force: true) if category end end - def rebuild_problem_users(limit: 10000) - user_ids = load_problem_user_ids(limit) - - if @verbose - puts "rebuilding #{user_ids.length} users" - end - - user_ids.each do |id| - user = User.find_by(id: id) - SearchIndexer.index(user, force: true) if user - end - end - - def rebuild_problem_topics(limit: 10000) - topic_ids = load_problem_topic_ids(limit) - - if @verbose - puts "rebuilding #{topic_ids.length} topics" - end - - topic_ids.each do |id| - topic = Topic.find_by(id: id) - SearchIndexer.index(topic, force: true) if topic - end - end - - def rebuild_problem_posts(limit: 20000, indexer: SearchIndexer, verbose: false) - post_ids = load_problem_post_ids(limit) - verbose ||= @verbose - - if verbose - puts "rebuilding #{post_ids.length} posts" - end - - i = 0 - post_ids.each do |id| - # could be deleted while iterating through batch - if post = Post.find_by(id: id) - indexer.index(post, force: true) - i += 1 - - if verbose && i % 1000 == 0 - puts "#{i} posts reindexed" - end - end - end - end - - def rebuild_problem_tags(limit: 10000) + def rebuild_tags(limit: 1_000, indexer: SearchIndexer) tag_ids = load_problem_tag_ids(limit) - if @verbose - puts "rebuilding #{tag_ids.length} tags" - end + puts "rebuilding #{tag_ids.size} tags" if @verbose tag_ids.each do |id| tag = Tag.find_by(id: id) - SearchIndexer.index(tag, force: true) if tag + indexer.index(tag, force: true) if tag end end - private + def rebuild_topics(limit: 10_000, indexer: SearchIndexer) + topic_ids = load_problem_topic_ids(limit) - def clean_post_search_data - puts "cleaning up post search data" if @verbose + puts "rebuilding #{topic_ids.size} topics" if @verbose - PostSearchData - .joins("LEFT JOIN posts p ON p.id = post_search_data.post_id") - .where("p.raw = ''") - .delete_all - - DB.exec(<<~SQL, deleted_at: CLEANUP_GRACE_PERIOD) - DELETE FROM post_search_data - WHERE post_id IN ( - SELECT post_id - FROM post_search_data - LEFT JOIN posts ON post_search_data.post_id = posts.id - INNER JOIN topics ON posts.topic_id = topics.id - WHERE (topics.deleted_at IS NOT NULL - AND topics.deleted_at <= :deleted_at) OR ( - posts.deleted_at IS NOT NULL AND - posts.deleted_at <= :deleted_at - ) - - ) - SQL + topic_ids.each do |id| + topic = Topic.find_by(id: id) + indexer.index(topic, force: true) if topic + end end - def clean_topic_search_data + def rebuild_posts(limit: 20_000, indexer: SearchIndexer) + post_ids = load_problem_post_ids(limit) + + puts "rebuilding #{post_ids.size} posts" if @verbose + + post_ids.each do |id| + post = Post.find_by(id: id) + indexer.index(post, force: true) if post + end + end + + def rebuild_users(limit: 5_000, indexer: SearchIndexer) + user_ids = load_problem_user_ids(limit) + + puts "rebuilding #{user_ids.size} users" if @verbose + + user_ids.each do |id| + user = User.find_by(id: id) + indexer.index(user, force: true) if user + end + end + + def clean_topics puts "cleaning up topic search data" if @verbose - DB.exec(<<~SQL, deleted_at: CLEANUP_GRACE_PERIOD) - DELETE FROM topic_search_data - WHERE topic_id IN ( - SELECT topic_id - FROM topic_search_data - INNER JOIN topics ON topic_search_data.topic_id = topics.id - WHERE topics.deleted_at IS NOT NULL - AND topics.deleted_at <= :deleted_at - ) + # remove search data from deleted topics + + DB.exec(<<~SQL, deleted_at: @cleanup_grace_period) + DELETE FROM topic_search_data + WHERE topic_id IN ( + SELECT topic_id + FROM topic_search_data + LEFT JOIN topics ON topic_id = topics.id + WHERE topics.id IS NULL + OR (deleted_at IS NOT NULL AND deleted_at <= :deleted_at) + ) SQL end - def load_problem_post_ids(limit) - params = { - locale: SiteSetting.default_locale, - version: SearchIndexer::MIN_POST_REINDEX_VERSION, - limit: limit - } + def clean_posts + puts "cleaning up post search data" if @verbose - DB.query_single(<<~SQL, params) - SELECT - posts.id - FROM posts - JOIN topics ON topics.id = posts.topic_id - LEFT JOIN post_search_data pd - ON pd.locale = :locale - AND pd.version >= :version - AND pd.post_id = posts.id - WHERE pd.post_id IS NULL - AND posts.deleted_at IS NULL - AND topics.deleted_at IS NULL - AND posts.raw != '' - ORDER BY posts.id DESC - LIMIT :limit + # remove search data from deleted/empty posts + + DB.exec(<<~SQL, deleted_at: @cleanup_grace_period) + DELETE FROM post_search_data + WHERE post_id IN ( + SELECT post_id + FROM post_search_data + LEFT JOIN posts ON post_id = posts.id + JOIN topics ON posts.topic_id = topics.id + WHERE posts.id IS NULL + OR posts.raw = '' + OR (posts.deleted_at IS NOT NULL AND posts.deleted_at <= :deleted_at) + OR (topics.deleted_at IS NOT NULL AND topics.deleted_at <= :deleted_at) + ) SQL end def load_problem_category_ids(limit) - Category.joins(:category_search_data) - .where('category_search_data.locale != ? - OR category_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::CATEGORY_INDEX_VERSION) - .order('categories.id asc') - .limit(limit) - .pluck(:id) - end - - def load_problem_topic_ids(limit) - Topic.joins(:topic_search_data) - .where('topic_search_data.locale != ? - OR topic_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::TOPIC_INDEX_VERSION) - .order('topics.id desc') - .limit(limit) - .pluck(:id) - end - - def load_problem_user_ids(limit) - User.joins(:user_search_data) - .where('user_search_data.locale != ? - OR user_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::USER_INDEX_VERSION) - .order('users.id asc') + Category + .joins("LEFT JOIN category_search_data ON category_id = categories.id") + .where("category_search_data.locale IS NULL OR category_search_data.locale != ? OR category_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::CATEGORY_INDEX_VERSION) + .order("categories.id ASC") .limit(limit) .pluck(:id) end def load_problem_tag_ids(limit) - Tag.joins(:tag_search_data) - .where('tag_search_data.locale != ? - OR tag_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::TAG_INDEX_VERSION) - .order('tags.id asc') + Tag + .joins("LEFT JOIN tag_search_data ON tag_id = tags.id") + .where("tag_search_data.locale IS NULL OR tag_search_data.locale != ? OR tag_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::TAG_INDEX_VERSION) + .order("tags.id ASC") .limit(limit) .pluck(:id) end + + def load_problem_topic_ids(limit) + Topic + .joins("LEFT JOIN topic_search_data ON topic_id = topics.id") + .where("topic_search_data.locale IS NULL OR topic_search_data.locale != ? OR topic_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::TOPIC_INDEX_VERSION) + .order("topics.id DESC") + .limit(limit) + .pluck(:id) + end + + def load_problem_post_ids(limit) + Post + .joins(:topic) + .joins("LEFT JOIN post_search_data ON post_id = posts.id") + .where("posts.raw != ''") + .where("topics.deleted_at IS NULL") + .where("post_search_data.locale IS NULL OR post_search_data.locale != ? OR post_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::POST_INDEX_VERSION) + .order("posts.id DESC") + .limit(limit) + .pluck(:id) + end + + def load_problem_user_ids(limit) + User + .joins("LEFT JOIN user_search_data ON user_id = users.id") + .where("user_search_data.locale IS NULL OR user_search_data.locale != ? OR user_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::USER_INDEX_VERSION) + .order("users.id ASC") + .limit(limit) + .pluck(:id) + end + end end diff --git a/app/models/category_list.rb b/app/models/category_list.rb index 306dfa3d39..4f7d2c0610 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -109,7 +109,7 @@ class CategoryList < DraftableList to_delete << c end end - @categories.each { |c| c.subcategory_ids = subcategories[c.id] } + @categories.each { |c| c.subcategory_ids = subcategories[c.id] || [] } @categories.delete_if { |c| to_delete.include?(c) } end diff --git a/app/models/group.rb b/app/models/group.rb index 4c601847ee..696703c187 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -838,6 +838,11 @@ class Group < ActiveRecord::Base ) end + def message_count + return 0 unless self.has_messages + TopicAllowedGroup.where(group_id: self.id).joins(:topic).count + end + protected def name_format_validator diff --git a/app/models/notification.rb b/app/models/notification.rb index 9fbc6afc42..b5a07a32e2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -4,13 +4,14 @@ class Notification < ActiveRecord::Base belongs_to :user belongs_to :topic + has_one :shelved_notification + MEMBERSHIP_REQUEST_CONSOLIDATION_WINDOW_HOURS = 24 validates_presence_of :data validates_presence_of :notification_type scope :unread, lambda { where(read: false) } - scope :unprocessed, lambda { where(processed: false) } scope :recent, lambda { |n = nil| n ||= 10; order('notifications.created_at desc').limit(n) } scope :visible , lambda { joins('LEFT JOIN topics ON notifications.topic_id = topics.id') .where('topics.id IS NULL OR topics.deleted_at IS NULL') } @@ -283,10 +284,11 @@ class Notification < ActiveRecord::Base end def send_email - if skip_send_email - return update(processed: true) - end - NotificationEmailer.process_notification(self) unless user.do_not_disturb? + return if skip_send_email + + user.do_not_disturb? ? + ShelvedNotification.create(notification_id: self.id) : + NotificationEmailer.process_notification(self) end end diff --git a/app/models/shelved_notification.rb b/app/models/shelved_notification.rb new file mode 100644 index 0000000000..79e4bee249 --- /dev/null +++ b/app/models/shelved_notification.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ShelvedNotification < ActiveRecord::Base + belongs_to :notification + + def process + NotificationEmailer.process_notification(notification, no_delay: true) + end +end diff --git a/app/models/trust_level_setting.rb b/app/models/trust_level_setting.rb index 4660adc580..68b7e2b1d9 100644 --- a/app/models/trust_level_setting.rb +++ b/app/models/trust_level_setting.rb @@ -9,7 +9,7 @@ class TrustLevelSetting < EnumSiteSetting def self.values levels = TrustLevel.all - @values ||= valid_values.map { |x| + valid_values.map { |x| { name: x.is_a?(Integer) ? "#{x}: #{levels[x.to_i].name}" : x, value: x diff --git a/app/models/user.rb b/app/models/user.rb index 8b6f58d13c..315eac92d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1391,6 +1391,10 @@ class User < ActiveRecord::Base active_do_not_disturb_timings.maximum(:ends_at) end + def shelved_notifications + ShelvedNotification.joins(:notification).where("notifications.user_id = ?", self.id) + end + protected def badge_grant diff --git a/app/models/user_notification_schedule.rb b/app/models/user_notification_schedule.rb index 18892506db..f2346d4e17 100644 --- a/app/models/user_notification_schedule.rb +++ b/app/models/user_notification_schedule.rb @@ -18,10 +18,14 @@ class UserNotificationSchedule < ActiveRecord::Base scope :enabled, -> { where(enabled: true) } def create_do_not_disturb_timings(delete_existing: false) - user.do_not_disturb_timings.where(scheduled: true).destroy_all if delete_existing + destroy_scheduled_timings if delete_existing UserNotificationScheduleProcessor.create_do_not_disturb_timings_for(self) end + def destroy_scheduled_timings + user.do_not_disturb_timings.where(scheduled: true).destroy_all + end + private def has_valid_times diff --git a/app/models/user_search.rb b/app/models/user_search.rb index 447f5d4492..7498b230be 100644 --- a/app/models/user_search.rb +++ b/app/models/user_search.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true -# Searches for a user by username or full text or name (if enabled in SiteSettings) class UserSearch MAX_SIZE_PRIORITY_MENTION ||= 500 def initialize(term, opts = {}) - @term = term - @term_like = "#{term.downcase.gsub("_", "\\_")}%" + @term = term.downcase + @term_like = @term.gsub("_", "\\_") + "%" @topic_id = opts[:topic_id] @category_id = opts[:category_id] @topic_allowed_users = opts[:topic_allowed_users] @@ -15,131 +14,129 @@ class UserSearch @include_staged_users = opts[:include_staged_users] || false @limit = opts[:limit] || 20 @groups = opts[:groups] + + @topic = Topic.find(@topic_id) if @topic_id + @category = Category.find(@category_id) if @category_id + @guardian = Guardian.new(@searching_user) - @guardian.ensure_can_see_groups_members! @groups if @groups - @guardian.ensure_can_see_category! Category.find(@category_id) if @category_id - @guardian.ensure_can_see_topic! Topic.find(@topic_id) if @topic_id + @guardian.ensure_can_see_groups_members!(@groups) if @groups + @guardian.ensure_can_see_category!(@category) if @category + @guardian.ensure_can_see_topic!(@topic) if @topic end def scoped_users users = User.where(active: true) users = users.where(staged: false) unless @include_staged_users + users = users.not_suspended unless @searching_user&.staff? if @groups - users = users.joins("INNER JOIN group_users ON group_users.user_id = users.id") + users = users + .joins(:group_users) .where("group_users.group_id IN (?)", @groups.map(&:id)) end - unless @searching_user && @searching_user.staff? - users = users.not_suspended - end - # Only show users who have access to private topic - if @topic_id && @topic_allowed_users == "true" - topic = Topic.find_by(id: @topic_id) - - if topic.category && topic.category.read_restricted - users = users.includes(:secure_categories) - .where("users.admin = TRUE OR categories.id = ?", topic.category.id) - .references(:categories) - end - end - - users.limit(@limit) - end - - def filtered_by_term_users - users = scoped_users - - if @term.present? - if SiteSetting.enable_names? && @term !~ /[_\.-]/ - query = Search.ts_query(term: @term, ts_config: "simple") - - users = users.includes(:user_search_data) - .references(:user_search_data) - .where("user_search_data.search_data @@ #{query}") - .order(DB.sql_fragment("CASE WHEN username_lower LIKE ? THEN 0 ELSE 1 END ASC", @term_like)) - - else - users = users.where("username_lower LIKE :term_like", term_like: @term_like) - end + if @topic_allowed_users == "true" && @topic&.category&.read_restricted + users = users + .references(:categories) + .includes(:secure_categories) + .where("users.admin OR categories.id = ?", @topic.category_id) end users end + def filtered_by_term_users + if @term.blank? + scoped_users + elsif SiteSetting.enable_names? && @term !~ /[_\.-]/ + query = Search.ts_query(term: @term, ts_config: "simple") + + scoped_users + .includes(:user_search_data) + .where("user_search_data.search_data @@ #{query}") + .order(DB.sql_fragment("CASE WHEN username_lower LIKE ? THEN 0 ELSE 1 END ASC", @term_like)) + else + scoped_users.where("username_lower LIKE :term_like", term_like: @term_like) + end + end + def search_ids users = Set.new # 1. exact username matches if @term.present? - scoped_users.where(username_lower: @term.downcase) + exact_matches = scoped_users.where(username_lower: @term) + + # don't polute mentions with users who haven't shown up in over a year + exact_matches = exact_matches.where('last_seen_at > ?', 1.year.ago) if @topic_id || @category_id + + exact_matches .limit(@limit) .pluck(:id) .each { |id| users << id } - end - return users.to_a if users.length >= @limit + return users.to_a if users.size >= @limit # 2. in topic if @topic_id in_topic = filtered_by_term_users - .where('users.id IN (SELECT p.user_id FROM posts p WHERE topic_id = ?)', @topic_id) + .where('users.id IN (SELECT user_id FROM posts WHERE topic_id = ?)', @topic_id) if @searching_user.present? in_topic = in_topic.where('users.id <> ?', @searching_user.id) end in_topic - .order('last_seen_at DESC') - .limit(@limit - users.length) + .order('last_seen_at DESC NULLS LAST') + .limit(@limit - users.size) .pluck(:id) .each { |id| users << id } end - return users.to_a if users.length >= @limit + return users.to_a if users.size >= @limit - secure_category_id = nil + # 3. in category + secure_category_id = + if @category_id + DB.query_single(<<~SQL, @category_id).first + SELECT id + FROM categories + WHERE read_restricted + AND id = ? + SQL + elsif @topic_id + DB.query_single(<<~SQL, @topic_id).first + SELECT id + FROM categories + WHERE read_restricted + AND id IN (SELECT category_id FROM topics WHERE id = ?) + SQL + end - if @category_id - secure_category_id = DB.query_single(<<~SQL, @category_id).first - SELECT id FROM categories - WHERE read_restricted AND id = ? - SQL - elsif @topic_id - secure_category_id = DB.query_single(<<~SQL, @topic_id).first - SELECT id FROM categories - WHERE read_restricted AND id IN ( - SELECT category_id FROM topics - WHERE id = ? - ) - SQL - end - - # 3. category matches if secure_category_id - @searching_user.present? - category_groups = Group.where(<<~SQL, secure_category_id, MAX_SIZE_PRIORITY_MENTION) groups.id IN ( - SELECT group_id FROM category_groups - JOIN groups g ON group_id = g.id - WHERE - category_id = ? AND - user_count < ? + SELECT group_id + FROM category_groups + JOIN groups g ON group_id = g.id + WHERE category_id = ? + AND user_count < ? ) SQL - category_groups = category_groups.members_visible_groups(@searching_user) + if @searching_user.present? + category_groups = category_groups.members_visible_groups(@searching_user) + end in_category = filtered_by_term_users .where(<<~SQL, category_groups.pluck(:id)) users.id IN ( SELECT gu.user_id - FROM group_users gu - WHERE group_id IN (?) - LIMIT 200 + FROM group_users gu + WHERE group_id IN (?) + LIMIT 200 ) SQL @@ -148,17 +145,19 @@ class UserSearch end in_category - .order('last_seen_at DESC') - .limit(@limit - users.length) + .order('last_seen_at DESC NULLS LAST') + .limit(@limit - users.size) .pluck(:id) .each { |id| users << id } end - return users.to_a if users.length >= @limit + return users.to_a if users.size >= @limit + # 4. global matches if @term.present? - filtered_by_term_users.order('last_seen_at DESC') - .limit(@limit - users.length) + filtered_by_term_users + .order('last_seen_at DESC NULLS LAST') + .limit(@limit - users.size) .pluck(:id) .each { |id| users << id } end diff --git a/app/serializers/category_detailed_serializer.rb b/app/serializers/category_detailed_serializer.rb index 483f78f845..6b1e5083b2 100644 --- a/app/serializers/category_detailed_serializer.rb +++ b/app/serializers/category_detailed_serializer.rb @@ -26,10 +26,6 @@ class CategoryDetailedSerializer < BasicCategorySerializer is_uncategorized end - def include_subcategory_ids? - subcategory_ids.present? - end - def topics_day count_with_subcategories(:topics_day) end diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb index 53a6f0d93d..584e20b1f0 100644 --- a/app/serializers/group_show_serializer.rb +++ b/app/serializers/group_show_serializer.rb @@ -25,7 +25,8 @@ class GroupShowSerializer < BasicGroupSerializer :email_password, :imap_last_error, :imap_old_emails, - :imap_new_emails + :imap_new_emails, + :message_count def self.admin_or_owner_attributes(*attrs) attributes(*attrs) diff --git a/app/services/notification_emailer.rb b/app/services/notification_emailer.rb index a8b2406ca3..21e31e5ebb 100644 --- a/app/services/notification_emailer.rb +++ b/app/services/notification_emailer.rb @@ -125,7 +125,6 @@ class NotificationEmailer end def self.process_notification(notification, no_delay: false) - notification.update(processed: true) return if @disabled email_user = EmailUser.new(notification, no_delay: no_delay) diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb index 472a1eb715..970d1fff42 100644 --- a/app/services/user_anonymizer.rb +++ b/app/services/user_anonymizer.rb @@ -59,11 +59,12 @@ class UserAnonymizer ) end - @user.user_avatar.try(:destroy) - @user.single_sign_on_record.try(:destroy) - @user.oauth2_user_infos.try(:destroy_all) - @user.user_associated_accounts.try(:destroy_all) - @user.api_keys.find_each { |x| x.try(:destroy) } + @user.user_avatar&.destroy! + @user.single_sign_on_record&.destroy! + @user.oauth2_user_infos.destroy_all + @user.user_associated_accounts.destroy_all + @user.api_keys.destroy_all + @user.user_api_keys.destroy_all @user.user_emails.secondary.destroy_all @user_history = log_action diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 41593b84db..544b9883ca 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -200,7 +200,9 @@ class UserUpdater if saved if user_notification_schedule - user_notification_schedule.create_do_not_disturb_timings(delete_existing: true) + user_notification_schedule.enabled ? + user_notification_schedule.create_do_not_disturb_timings(delete_existing: true) : + user_notification_schedule.destroy_scheduled_timings end DiscourseEvent.trigger(:user_updated, user) end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 165ece79e1..796a7be265 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -60,8 +60,8 @@ <%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %> - <%- if !current_user && (data = cookies.delete(:authentication_data, path: Discourse.base_path("/"))) %> - + <%- if authentication_data %> + <%- end %> diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 32df70d8cb..b2338abe47 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -1873,8 +1873,6 @@ ar: date_time_picker: from: من to: إلى - errors: - to_before_from: "يجب أن يكون تاريخ الانتهاء متأخراً عن التاريخ." emoji_picker: filter_placeholder: ابحث عن إيموجي smileys_&_emotion: الابتسامات والعواطف diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 793db26e22..d5ae6e75e0 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -1614,8 +1614,6 @@ bs_BA: date_time_picker: from: Od to: Ka - errors: - to_before_from: "Ciljani datum mora biti kasniji od početnog datuma." emoji_picker: filter_placeholder: Pretraži emotikone smileys_&_emotion: Smeješci i osječaj diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index 88aad82005..462344ec75 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -1479,8 +1479,6 @@ ca: date_time_picker: from: De to: A - errors: - to_before_from: "La data final ha de ser posterior a la data inicial." emoji_picker: filter_placeholder: Cerca un emoji smileys_&_emotion: Emoticones diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 8d049b6f4a..d69faf7ea1 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -1390,8 +1390,6 @@ da: date_time_picker: from: Fra to: Til - errors: - to_before_from: "Til dato skal være senere end fra dato." emoji_picker: filter_placeholder: Søg efter humørikon smileys_&_emotion: Smilys og følelser diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 32da447236..702cdd1c25 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -187,6 +187,7 @@ de: us_gov_west_1: "AWS GovCloud (US-West)" us_west_1: "USA West (Nordkalifornien)" us_west_2: "USA West (Oregon)" + clear_input: "Eingabe löschen" edit: "Titel und Kategorie dieses Themas ändern" expand: "Aufklappen" not_implemented: "Entschuldige, diese Funktion wurde noch nicht implementiert!" @@ -1673,8 +1674,6 @@ de: date_time_picker: from: Von to: An - errors: - to_before_from: "Das Datum muss später als das von-Datum sein." emoji_picker: filter_placeholder: Emoji suchen smileys_&_emotion: Smileys und Emotion @@ -1883,7 +1882,7 @@ de: membership_request_accepted: "Mitgliedschaft akzeptiert in '%{group_name}' " membership_request_consolidated: one: "%{count} offene Mitgliedschaftsanfrage für '%{group_name}'" - other: "%{count} offene Mitgliedschaftanfragen für '%{group_name}'" + other: "%{count} offene Mitgliedschaftsanfragen für '%{group_name}'" reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - abgeschlossen" @@ -1966,6 +1965,7 @@ de: or_search_google: "Oder versuche stattdessen mit Google zu suchen:" search_google: "Versuche stattdessen mit Google zu suchen:" search_google_button: "Google" + search_button: "Suche" context: user: "Beiträge von @%{username} durchsuchen" category: "Kategorie #%{category} durchsuchen" @@ -2864,6 +2864,7 @@ de: list_filters: all: "alle Themen" none: "keine Unterkategorien" + colors_disabled: "Sie können keine Farben auswählen, da Sie keinen Kategoriestil haben." flagging: title: "Danke für deine Mithilfe!" action: "Beitrag melden" @@ -3305,6 +3306,7 @@ de: two_hours: "2 Stunden" tomorrow: "Bis morgen" custom: "Benutzerdefiniert" + set_schedule: "Zeitplan für Benachrichtigungen festlegen" admin_js: type_to_filter: "zum Filtern hier eingeben…" admin: @@ -3332,6 +3334,10 @@ de: installed_version: "Installiert" latest_version: "Neueste" problems_found: "Ein paar Ratschläge basierend auf deinen aktuellen Website-Einstellungen" + new_features: + title: "\U0001F381 Neue Funktionen" + dismiss: "Verwerfen" + learn_more: "Mehr erfahren" last_checked: "Zuletzt geprüft" refresh_problems: "Aktualisieren" no_problems: "Es wurden keine Probleme gefunden." @@ -3462,6 +3468,9 @@ de: group_members: "Gruppenmitglieder" delete: "Löschen" delete_confirm: "Diese Gruppe löschen?" + delete_with_messages_confirm: + one: "Wenn Sie diese Gruppe löschen, wird %{count} Nachricht verwaist, Gruppenmitglieder haben keinen Zugriff mehr darauf.

    Bist du sicher?" + other: "Wenn Sie diese Gruppe löschen, werden %{count} Nachrichten verwaist, Gruppenmitglieder haben keinen Zugriff mehr auf sie.

    Bist du sicher?" delete_failed: "Gruppe konnte nicht gelöscht werden. Wenn dies eine automatische Gruppe ist, kann sie nicht gelöscht werden." delete_owner_confirm: "Eigentümerrechte für '%{username}' entfernen?" add: "Hinzufügen" @@ -3883,9 +3892,7 @@ de: Beispiel: - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } + %{example} Eigenschaften sollten mit einem Präfix versehen werden, um Konflikte mit Discourse und Plugins zu vermeiden. head_tag: @@ -4382,6 +4389,9 @@ de: anonymize_yes: "Ja, diesen Benutzer anonymisieren" anonymize_failed: "Beim Anonymisieren des Benutzers ist ein Fehler aufgetreten." delete: "Benutzer löschen" + delete_posts: + progress: + title: "Fortschritt beim Löschen von Beiträgen" merge: button: "Zusammenführen" prompt: @@ -4393,6 +4403,8 @@ de: target_username_placeholder: "Benutzername des neuen Eigentümers" transfer_and_delete: "Übertragen & Löschen @%{username}" cancel: "Abbrechen" + progress: + title: "Fortschritt der Zusammenführung" confirmation: title: "Übertragen & Löschen @%{username}" description: | @@ -4540,6 +4552,7 @@ de: recommended: "Wir empfehlen, dass du den folgenden Text an deine Bedürfnisse anpasst:" show_overriden: "Nur geänderte Texte anzeigen" locale: "Sprache:" + fallback_locale_warning: "Du bearbeitest eine Sprache basierend auf %{fallback}. Benutzer, die %{fallback} als Sprache ihrer Schnittstelle wählen, werden deine Änderungen nicht sehen." more_than_50_results: "Es gibt mehr als 50 Ergebnisse. Bitte grenze deine Suche weiter ein." settings: show_overriden: "Nur geänderte anzeigen" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 000fb2fe66..0e767ed114 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -1594,8 +1594,6 @@ el: date_time_picker: from: Από to: Προς - errors: - to_before_from: "Η τελική ημερομηνία πρέπει να είναι αργότερα από την αρχική ημερομηνία." emoji_picker: filter_placeholder: Αναζήτηση για emoji smileys_&_emotion: Χαμόγελα και συγκίνηση diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fefb293a43..e3d88511e1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -210,6 +210,7 @@ en: us_west_1: "US West (N. California)" us_west_2: "US West (Oregon)" + clear_input: "Clear input" edit: "edit the title and category of this topic" expand: "Expand" not_implemented: "That feature hasn't been implemented yet, sorry!" @@ -837,7 +838,6 @@ en: posts: "Posts" topics: "Topics" latest: "Latest" - latest_by: "latest by" toggle_ordering: "toggle ordering control" subcategories: "Subcategories" muted: "Muted categories" @@ -3600,6 +3600,10 @@ en: installed_version: "Installed" latest_version: "Latest" problems_found: "Some advice based on your current site settings" + new_features: + title: "🎁 New Features" + dismiss: "Dismiss" + learn_more: "Learn more" last_checked: "Last checked" refresh_problems: "Refresh" no_problems: "No problems were found." @@ -3734,6 +3738,9 @@ en: group_members: "Group members" delete: "Delete" delete_confirm: "Delete this group?" + delete_with_messages_confirm: + one: "Deleting this group will cause %{count} message to be orphaned, group members will no longer have access to it.

    Are you sure?" + other: "Deleting this group will cause %{count} messages to be orphaned, group members will no longer have access to them.

    Are you sure?" delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed." delete_owner_confirm: "Remove owner privilege for '%{username}'?" add: "Add" @@ -4162,9 +4169,7 @@ en: Example: - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } + %{example} Prefixing the property names is highly recommended to avoid conflicts with plugins and/or core. head_tag: diff --git a/config/locales/client.en_GB.yml b/config/locales/client.en_GB.yml index 1c76c9c06f..d299959136 100644 --- a/config/locales/client.en_GB.yml +++ b/config/locales/client.en_GB.yml @@ -112,17 +112,6 @@ en_GB: color_definitions: text: "Colour Definitions" title: "Enter custom colour definitions (advanced users only)" - placeholder: |2- - - Use this stylesheet to add custom colours to the list of CSS custom properties. - - Example: - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } - - Prefixing the property names is highly recommended to avoid conflicts with plugins and/or core. colors: select_base: title: "Select base colour palette" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 922da8a1e0..45878c36e1 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -424,6 +424,9 @@ es: fields: "Campos" reject_reason: "Motivo" user_percentage: + summary: + one: "%{agreed}, %{disagreed}, %{ignored}, (del último reporte)" + other: "%{agreed}, %{disagreed}, %{ignored} (de los últimos %{count} reportes)" agreed: one: "%{count}% de acuerdo" other: "%{count}% de acuerdo" @@ -841,6 +844,20 @@ es: mute_option_title: "No recibirás ninguna notificación relacionada con este usuario." normal_option: "Normal" normal_option_title: "Se te notificará si este usuario te responde, cita o menciona." + notification_schedule: + title: "Horario de notificaciones" + label: "Activar horario personalizado de notificaciones" + tip: "Fuera de estas horas, se activará el modo «no molestar» automáticamente." + midnight: "Medianoche" + none: "Ninguno" + monday: "Lunes" + tuesday: "Martes" + wednesday: "Miércoles" + thursday: "Jueves" + friday: "Viernes" + saturday: "Sábado" + sunday: "Domingo" + to: "a" activity_stream: "Actividad" preferences: "Preferencias" feature_topic_on_profile: @@ -870,6 +887,7 @@ es: perm_denied_expl: "Has denegado el permiso para las notificaciones. Configura tu navegador para permitir notificaciones. " disable: "Desactivar notificaciones" enable: "Activar notificaciones" + each_browser_note: 'Nota: hay que cambiar este ajuste en cada navegador que uses. Todas las notificaciones serán desactivadas en el modo «no molestar», independientemente de este ajuste.' consent_prompt: "¿Quieres recibir notificaciones en vivo cuando alguien responda a tus mensajes?" dismiss: "Descartar" dismiss_notifications: "Descartar todo" @@ -1654,8 +1672,6 @@ es: date_time_picker: from: Desde to: Hasta - errors: - to_before_from: "La fecha «hasta» debe ser posterior a la fecha «desde»." emoji_picker: filter_placeholder: Buscar emoji smileys_&_emotion: Caras y emociones @@ -1814,6 +1830,7 @@ es: label: "Crear tema" shared_draft: label: "Borrador compartido" + desc: "Inicia un tema-borrador que solo será visible por los usuarios permitidos" toggle_topic_bump: label: "Alternar bump del tema" desc: "Responder sin alterar la fecha de última respuesta" @@ -1946,6 +1963,7 @@ es: or_search_google: "O prueba buscar a través de Google:" search_google: "Intenta buscar con Google:" search_google_button: "Google" + search_button: "Buscar" context: user: "Buscar publicaciones de @%{username}" category: "Buscar la categoría #%{category}" @@ -2514,7 +2532,7 @@ es: other: "ver %{count} publicaciones ocultas" notice: new_user: "Esta es la primera vez que %{user} ha publicado — ¡démosle la bienvenida a nuestra comunidad!" - returning_user: "Hace tiempo que no vemos a %{user} — su última publicación fue %{time}. " + returning_user: "Hacía tiempo que no veíamos a %{user}, su última publicación fue %{time}." unread: "Publicaciones sin leer" has_replies: one: "%{count} Respuesta" @@ -2843,6 +2861,7 @@ es: list_filters: all: "todos los temas" none: "sin subcategoría" + colors_disabled: "No puedes seleccionar colores porque tienes no tienes activados los estilos de categoría." flagging: title: "¡Gracias por ayudar a mantener una comunidad civilizada!" action: "Reportar publicación" @@ -2860,6 +2879,7 @@ es: notify_action: "Mensaje" official_warning: "Advertencia oficial" delete_spammer: "Eliminar spammer" + delete_confirm_MF: "Estás a punto de eliminar {POSTS, plural, one {# publicación} other {# publicaciones}} y {TOPICS, plural, one {# tema} other {# temas}} de este usuario, también eliminarás su cuenta, bloquearás registros desde su dirección IP {ip_address}, y añadirás su correo electrónico {email} a una lista de bloqueos permanentes. ¿Seguro que el usuario es de verdad un spammer?" yes_delete_spammer: "Sí, eliminar spammer" ip_address_missing: "(N/D)" hidden_email_address: "(oculto)" @@ -2926,6 +2946,12 @@ es: title: "Este tema es un mensaje personal" help: "Este tema es un mensaje personal" posts: "Publicaciones" + posts_likes_MF: | + Este tema tiene {count, plural, one {# respuesta} other {# respuestas}} {ratio, select, + low {con una proporción de me gusta por publicación alta} + med {con una proporción de me gusta por publicación muy alta} + high {con una proporción de me gusta por publicación extremadamente alta} + other {}} original_post: "Publicación original" views: "Vistas" views_lowercase: @@ -3255,6 +3281,7 @@ es: invite: custom_message: "Dale a tu invitación un toque personal escribiendo un mensaje personalizado." custom_message_placeholder: "Ingresa tu mensaje personalizado" + approval_not_required: "El usuario será aprobado automáticamente en cuanto acepte esta invitación." custom_message_template_forum: "¡Hey, deberías unirte a este foro!" custom_message_template_topic: "¡Hey, creemos que este tema te va a encantar!" forced_anonymous: "Debido a una carga extrema, esto se está mostrando temporalmente a todos como lo vería un usuario que no haya iniciado sesión." @@ -3276,6 +3303,7 @@ es: two_hours: "2 horas" tomorrow: "Hasta mañana" custom: "Personalizado" + set_schedule: "Establecer un horario de notificaciones" admin_js: type_to_filter: "filtrar opciones..." admin: @@ -3303,6 +3331,10 @@ es: installed_version: "Instalada" latest_version: "Última" problems_found: "Algunos consejos con base en tus ajustes actuales" + new_features: + title: "\U0001F381 Novedades" + dismiss: "Ocultar" + learn_more: "Más información" last_checked: "Ultima comprobación" refresh_problems: "Refrescar" no_problems: "No se encontró ningún problema." @@ -3793,6 +3825,7 @@ es: install_upload: "Desde tu dispositivo" install_git_repo: "Desde un repositorio git" install_create: "Crear nuevo" + duplicate_remote_theme: "El componente de tema «%{name}» ya está instalado, ¿seguro que quieres instalar otra copia?" about_theme: "Acerca de" license: "Licencia" version: "Versión:" @@ -3812,6 +3845,7 @@ es: check_for_updates: "Buscar actualizaciones" updating: "Actualizando..." up_to_date: "El tema está actualizado. Última comprobación:" + has_overwritten_history: "La versión actual del tema ya no existe, porque el historial de Git ha sido sobreescrito por un force push." add: "Agregar" theme_settings: "Ajustes del tema" no_settings: "Este tema no tiene ajustes." @@ -3841,17 +3875,6 @@ es: color_definitions: text: "Definiciones de color" title: "Introducir definiciones de colores personalizadas (para usuarios avanzados)" - placeholder: |2- - - Usa esta hoja de estilos para añadir colores personalizados a la lista de propiedades personalizadas de CSS. - - Por ejemplo: - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } - - Añadir un prefijo a los nombres de las propiedades es altamente recomendado para evitar conflictos con plugins y el núcleo. head_tag: text: "" title: "HTML que será insertado antes de la etiqueta" @@ -4297,6 +4320,7 @@ es: clear_penalty_history: title: "Eliminado historial de faltas" description: "usuarios con faltas no pueden alcanzar NC3" + delete_all_posts_confirm_MF: "Estás a punto de eliminar {POSTS, plural, one {# publicación} other {# publicaciones}} y {TOPICS, plural, one {# tema} other {# temas}}. ¿Estás seguro?" silence: "Silenciar" unsilence: "Dejar de silenciar" silenced: "¿Silenciado?" @@ -4345,6 +4369,9 @@ es: anonymize_yes: "Sí, hacer anónima esta cuenta." anonymize_failed: "Hubo un problema al hacer anónima la cuenta." delete: "Eliminar usuario" + delete_posts: + progress: + title: "Progreso de la eliminación de publicaciones" merge: button: "Juntar" prompt: @@ -4356,6 +4383,8 @@ es: target_username_placeholder: "Nombre de usuario del nuevo dueño" transfer_and_delete: "Transferir y eliminar @%{username}" cancel: "Cancelar" + progress: + title: "Progreso de fusión" confirmation: title: "Transferir y eliminar @%{username}" description: | @@ -4502,6 +4531,7 @@ es: go_back: "Volver a la búsqueda" recommended: "Recomendamos personalizar el siguiente texto para que se ajusten a tus necesidades:" show_overriden: "Solo mostrar sobrescritos" + locale: "Idioma:" more_than_50_results: "Hay más de 50 resultados. Por favor, afina tu busqueda." settings: show_overriden: "Solo mostrar anulados" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index f06d408af5..8ea48d0093 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -1603,8 +1603,6 @@ fi: date_time_picker: from: Mistä to: Vastaanottaja - errors: - to_before_from: "Päättymispäivän on oltava myöhempi kuin alkamispäivän." emoji_picker: filter_placeholder: Etsi emojia smileys_&_emotion: Hymiöt ja tunteet diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 8dada8822d..dfb9ba7e74 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -844,6 +844,19 @@ fr: mute_option_title: "Vous ne recevrez pas de notifications en lien avec cet utilisateur." normal_option: "Normal" normal_option_title: "Vous serez notifié si cet utilisateur vous répond, vous cite ou vous mentionne." + notification_schedule: + title: "Planification des notifications" + label: "Activer la planification des notifications personnalisées" + tip: "En dehors de ces heures, vous serez automatiquement mis en \"ne pas déranger\"." + midnight: "Minuit" + none: "Aucun" + monday: "Lundi" + tuesday: "Mardi" + wednesday: "Mercredi" + thursday: "Jeudi" + friday: "Vendredi" + saturday: "samedi" + sunday: "dimanche" activity_stream: "Activité" preferences: "Préférences" feature_topic_on_profile: @@ -890,7 +903,7 @@ fr: color_schemes: default_description: "Couleur du thème" disable_dark_scheme: "Désactivé" - dark_instructions: "Vous pouvez prévisualiser le jeu de couleurs du mode sombre en basculant le mode sombre de votre appareil." + dark_instructions: "Vous pouvez prévisualiser le jeu de couleurs du mode sombre en activant le mode sombre de votre appareil." undo: "Réinitialiser" regular: "Normal" dark: "Mode sombre" @@ -1659,8 +1672,6 @@ fr: date_time_picker: from: De to: À - errors: - to_before_from: "La date de fin doit être postérieure à celle de début." emoji_picker: filter_placeholder: Rechercher un émoji smileys_&_emotion: Frimousses et émotions @@ -3292,6 +3303,7 @@ fr: two_hours: "2 heures" tomorrow: "Jusqu'à demain" custom: "Personnalisé" + set_schedule: "Planifier les notifications" admin_js: type_to_filter: "commencez à taper pour filtrer…" admin: @@ -3319,6 +3331,10 @@ fr: installed_version: "Installée" latest_version: "Dernière" problems_found: "Quelques conseils d'après vos paramètres actuels" + new_features: + title: "\U0001F381 Nouvelles fonctionnalités" + dismiss: "Ignorer" + learn_more: "En savoir plus…" last_checked: "Dernière vérification" refresh_problems: "Actualiser" no_problems: "Aucun problème n'a été trouvé." @@ -3862,17 +3878,6 @@ fr: color_definitions: text: "Définitions de couleur" title: "Entrez des définitions de couleurs personnalisées (utilisateurs avancés uniquement)" - placeholder: |2- - - Utilisez cette feuille de style pour ajouter des couleurs personnalisées à la liste des propriétés personnalisées CSS. - - Exemple : - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } - - Il est fortement recommandé de préfixer les noms de propriété pour éviter les conflits avec Discourse ou ses extensions. head_tag: text: "" title: "HTML qui sera inséré avant la balise " diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index a4c650dfca..b6ba6056ea 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -187,6 +187,7 @@ gl: us_gov_west_1: "AWS GovCloud (EUA-Oeste)" us_west_1: "EUA Oeste (N. California)" us_west_2: "EUA Oeste (Oregón)" + clear_input: "Despexar entrada" edit: "editar o título e a categoría deste tema" expand: "Expandir" not_implemented: "Sentímolo pero esta funcionalidade non se implementou aínda." @@ -422,6 +423,7 @@ gl: email: "Correo electrónico" name: "Nome" fields: "Campos" + reject_reason: "Razón" user_percentage: summary: one: "%{agreed}, %{disagreed}, %{ignored} (da última alerta)" @@ -514,6 +516,9 @@ gl: other: "Tes %{count} publicacións pendentes." ok: "De acordo" example_username: "nome de usuario" + reject_reason: + title: "Por que rexeita a este usuario?" + send_email: "Enviar correo de rexeitamento" user_action: user_posted_topic: "%{user} publicou o tema" you_posted_topic: "Vostede publicou o tema" @@ -840,6 +845,20 @@ gl: mute_option_title: "Non recibirás notificación ningunha relacionada con este usuario." normal_option: "Normal" normal_option_title: "Recibirás unha notificación se este usuario che responde, cita a túa mensaxe ou menciona o teu nome." + notification_schedule: + title: "Calendario de notificacións" + label: "Activar o calendario personalizado de notificacións" + tip: "Fóra destas horas, vostede terá posto automaticamente en «non molestar»" + midnight: "Medianoite" + none: "Ningunha" + monday: "Luns" + tuesday: "Martes" + wednesday: "Mércores" + thursday: "Xoves" + friday: "Venres" + saturday: "Sábado" + sunday: "Domingo" + to: "para o" activity_stream: "Actividade" preferences: "Preferencias" feature_topic_on_profile: @@ -1655,8 +1674,6 @@ gl: date_time_picker: from: De to: A - errors: - to_before_from: "A data límite debe ser posterior á de orixe." emoji_picker: filter_placeholder: Buscar emoji smileys_&_emotion: Emoticonas e emoción @@ -1948,6 +1965,7 @@ gl: or_search_google: "Ou proba a buscar con Google:" search_google: "Proba a buscar con Google:" search_google_button: "Google" + search_button: "Buscar" context: user: "Buscar publicacións de @%{username}" category: "Buscar na categoría #%{category}" @@ -2846,6 +2864,7 @@ gl: list_filters: all: "todos os temas" none: "sen subcategorías" + colors_disabled: "Non pode seleccionar cores porque ten un estilo de categoría con ningún." flagging: title: "Grazas por axudar a manter a nosa comunidade." action: "Denunciar publicación" @@ -3287,6 +3306,7 @@ gl: two_hours: "2 horas" tomorrow: "Ata mañá" custom: "Personalizado" + set_schedule: "Estabelecer un calendario de notificacións" admin_js: type_to_filter: "escribe para filtrar..." admin: @@ -3314,6 +3334,10 @@ gl: installed_version: "Instalado" latest_version: "Últimos" problems_found: "Algúns consellos baseados na súa configuración actual do sitio" + new_features: + title: "Novas funcionalidades" + dismiss: "Desbotar" + learn_more: "Saber máis" last_checked: "Última comprobación" refresh_problems: "Actualizar" no_problems: "Non se atoparon problemas." @@ -3444,6 +3468,9 @@ gl: group_members: "Membros do grupo" delete: "Eliminar" delete_confirm: "Eliminar este grupo?" + delete_with_messages_confirm: + one: "Se elimina este grupo, quedará orfo %{count} mensaxe, os membros do grupo xa non terán acceso a el.

    Está seguro?" + other: "Se elimina este grupo, quedará orfo %{count} mensaxes, os membros do grupo xa non terán acceso a el.

    Está seguro?" delete_failed: "Non é posíbel eliminar o grupo. Se este é un grupo automático, non se pode destruír." delete_owner_confirm: "Eliminar os privilexios de usuario de «%{username}»?" add: "Engadir" @@ -3861,15 +3888,13 @@ gl: title: "Escriba definicións de cor personalizadas (só para usuarios avanzados)" placeholder: |2- - Utilice esta folla de estilo para engadir cores personalizadas á listaxe de propiedades CSS. + Utilice esta folla de estilo para engadir cores personalizadas á lista de propiedades de CSS personalizadas Exemplo: - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } + %{example} - Prefixar os nomes da propiedade resulta altamente recomendábel para evitarmos conflitos con complementos e/ou o núcleo. + Recoméndase intensamente prefixar os nomes para evitar conflitos con complementos e/ou o núcleo. head_tag: text: "" title: "HTML que se inserirá antes da etiqueta " @@ -4364,6 +4389,9 @@ gl: anonymize_yes: "Si, converter a conta en anónima" anonymize_failed: "Produciuse un problema convertendo a conta en anónima." delete: "Eliminar usuario" + delete_posts: + progress: + title: "Progreso na eliminación de publicacións" merge: button: "Combinar" prompt: @@ -4375,6 +4403,8 @@ gl: target_username_placeholder: "Nome de usuario do novo propietario" transfer_and_delete: "Transferir e eliminar @%{username}" cancel: "Cancelar" + progress: + title: "Combinar o progreso" confirmation: title: "Transferir e eliminar @%{username}" description: | @@ -4521,6 +4551,8 @@ gl: go_back: "Volver á busca" recommended: "Recomendamos personalizar o seguinte texto para axeitalo ás túas necesidades:" show_overriden: "Amosar só os cambios" + locale: "Idioma:" + fallback_locale_warning: "Está a editar un idioma baseado en %{fallback}. Os usuarios que escollan %{fallback} como idioma de interface non verán os cambios." more_than_50_results: "Hai máis de 50 resultados. Restrinxe a busca." settings: show_overriden: "Amosar só os cambios" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 7466ef4396..c4242b623f 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -233,6 +233,7 @@ he: us_gov_west_1: "הענן הממשלתי של AWS (מערב ארה״ב)" us_west_1: "מערב ארה״ב (צפון קליפורניה)" us_west_2: "מערב ארה״ב (אורגון)" + clear_input: "פינוי הקלט" edit: "עריכת הכותרת והקטגוריה של נושא זה" expand: "הרחב" not_implemented: "תכונה זו עדיין לא מומשה, עמך הסליחה!" @@ -1788,8 +1789,6 @@ he: date_time_picker: from: מאת to: אל - errors: - to_before_from: "תאריך היעד חייב להיות לאחר תאריך ההתחלה." emoji_picker: filter_placeholder: חיפוש אחר אמוג׳י smileys_&_emotion: חייכנים ורגש @@ -3599,6 +3598,10 @@ he: installed_version: "הותקן" latest_version: "אחרונה" problems_found: "עצה שמבוססת על הגדרות האתר הנוכחיות שלך" + new_features: + title: "\U0001F381 תכונות חדשות" + dismiss: "התעלמות" + learn_more: "מידע נוסף" last_checked: "נבדק לאחרונה" refresh_problems: "רענן" no_problems: "לא נמצאו בעיות." @@ -3731,6 +3734,11 @@ he: group_members: "חברי הקבוצה" delete: "מחיקה" delete_confirm: "להסיר קבוצה זו?" + delete_with_messages_confirm: + one: "מחיקת הקבוצה הזאת תוביל להתייתמותה של הודעה %{count}, חברי הקבוצה לא יוכלו לגשת אליה עוד.

    להמשיך?" + two: "מחיקת הקבוצה הזאת תוביל להתייתמותן של %{count} הודעות, חברי הקבוצה לא יוכלו לגשת אליהן עוד.

    להמשיך?" + many: "מחיקת הקבוצה הזאת תוביל להתייתמותן של %{count} הודעות, חברי הקבוצה לא יוכלו לגשת אליהן עוד.

    להמשיך?" + other: "מחיקת הקבוצה הזאת תוביל להתייתמותן של %{count} הודעות, חברי הקבוצה לא יוכלו לגשת אליהן עוד.

    להמשיך?" delete_failed: "לא ניתן להסיר קבוצה זו. אם זו קבוצה אוטומטית, היא בלתי ניתנת למחיקה." delete_owner_confirm: "להסיר את הרשאות הניהול של ‚%{username}’?" add: "הוספה" @@ -4158,17 +4166,15 @@ he: דוגמה: - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } + %{example} מומלץ מאוד להוסיף קידומת לשמות המאפיינים כדי להימנע מסתירות מול תוספים ו/או הליבה. head_tag: - text: "" - title: "HTML שיוכנס לפני התג " + text: "" + title: "HTML שיוכנס לפני התג " body_tag: - text: "" - title: "HTML שיוכנס לפני התג " + text: "" + title: "HTML שיוכנס לפני התג " yaml: text: "YAML" title: "עריכת הגדרות ערכת העיצוב בתצורת YAML" @@ -4830,6 +4836,7 @@ he: recommended: "אנו ממליצים לערוך את הטקסט הבא כדי שיתאים לצרכים שלך:" show_overriden: "להציג רק את אלו שנדרסו" locale: "שפה:" + fallback_locale_warning: "בחרת לערוך שפה שמבוססת על %{fallback}. משתמשים שבוחרים ב%{fallback} כשפת מנשק המשתמש שלהם לא יראו את השינויים שלך." more_than_50_results: "יש למעלה מ־50 תוצאות. נא למקד את החיפוש שלך." settings: show_overriden: "להציג רק את אלו שנדרסו" diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index 03bda859c8..0353042107 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -476,6 +476,7 @@ hu: sent_by_user: "%{user} küldte" sent_by_you: "Ön küldte" directory: + username: "Felhasználónév" filter_name: "szűrés felhasználónév alapján" title: "Felhasználók" likes_given: "Adott" @@ -782,60 +783,59 @@ hu: text_size_default_on_all_devices: "Legyen ez az alapértelmezett szövegméret az összes eszközén" allow_private_messages: "Engedélyezés a többi felhasználónak, hogy privát üzenetet küldjenek Önnek" external_links_in_new_tab: "Az összes külső hivatkozás megnyitása új lapon" - enable_quoting: "A kijelölt szöveg engedélyezése idézetként" - enable_defer: "Engedélyezze a témák olvasatlanként jelölését" - change: "megváltoztatás" + enable_quoting: "A kijelölt szöveg idézése a válaszban" + enable_defer: "A témák olvasatlannak jelölésének engedélyezése" + change: "módosítás" featured_topic: "Kiemelt téma" - moderator: "{user}} egy moderátor" - admin: "{user}} egy adminisztrátor" + moderator: "%{user} egy moderátor" + admin: "%{user} egy adminisztrátor" moderator_tooltip: "Ez a felhasználó egy moderátor" admin_tooltip: "Ez a felhasználó egy adminisztrátor" silenced_tooltip: "Ez a felhasználó némítva van" - suspended_notice: "Ez a felhasználó fel van függesztve eddig: %{date}." - suspended_permanently: "Ez a felhasználó fel van függesztve." - suspended_reason: "Ok:" - email_activity_summary: "Aktivitás Összefoglaló" + suspended_notice: "Ez a felhasználó eddig felfüggesztett: %{date}." + suspended_permanently: "Ez a felhasználó felfüggesztett." + suspended_reason: "Ok: " + email_activity_summary: "Tevékenység-összefoglaló" mailing_list_mode: label: "Levelezőlista mód" enabled: "Levelezőlista mód bekapcsolása" - instructions: |2 + instructions: | + Ez a beállítás felülbírálja a tevékenység-összefoglalót.
    - Ez a beállítás felülírja az aktivitás összefoglalót.
    - - Némított témák és kategóriák nem lesznek benne ezekben az emailekben. - individual: "Küldjön e-mailt minden új hozzászólásról" - individual_no_echo: "Küldjön e-mailt minden új hozzászólásról kivéve a sajátokat" - many_per_day: "Küldjön e-mailt minden új hozzászólás után (naponta kb. %{dailyEmailEstimate} db)" - few_per_day: "Küldjön e-mailt minden új hozzászólás után (naponta kb. 2 db)" + A némított témák és kategóriák nem lesznek benne ezekben az e-mailekben. + individual: "E-mail küldése az összes új hozzászólásról" + individual_no_echo: "E-mail küldése az összes új hozzászólásról, kivéve a sajátokat" + many_per_day: "E-mail küldése az összes új hozzászólásról (naponta kb. %{dailyEmailEstimate} db)" + few_per_day: "E-mail küldése az összes új hozzászólásról (naponta kb. 2 db)" warning: "Levelezőlista üzemmód bekapcsolva. Az e-mail értesítések beállításai felülbírálva." tag_settings: "Címkék" - watched_tags: "Figyelve" - watched_tags_instructions: "Automatikusan figyeltetni fogod az összes ezen a címkékkel ellátott témát. Értesítést fogsz kapni az összes új hozzászólásról és témáról, és az új hozzászólások száma megfog jelenni a téma mellett." - tracked_tags: "Követve" - tracked_tags_instructions: "Automatikusan követni fogsz minden ilyen címkéjű témakört. Az új hozzászólások száma megfog jelenni a téma mellett." - muted_tags: "Némítva" - muted_tags_instructions: "Egyáltalán nem fogsz kapni semmilyen értesítést ennek a kategóriának a témaköreiről és legújabbak között sem fog megjelenni." - watched_categories: "Figyelve" - watched_categories_instructions: "Automatikusan figyelni fogod ezeket a kategóriákat. Értesítést fogsz kapni minden új témáról és bejegyzésről, a témakörök neve mellett meg fog jelenni az új bejegyzések számlálója is." - tracked_categories: "Követve" - tracked_categories_instructions: "Automatikusan nyomon fog követni minden új témát ezekben a kategóriákban " - watched_first_post_categories: "Első bejegyzés megtekintése" - watched_first_post_categories_instructions: "Csak minden új témakör legelső hozzászólásáról fogsz értesítést kapni ezekben a kategóriákban." - watched_first_post_tags: "Első bejegyzés megtekintése" - watched_first_post_tags_instructions: "Az új témakör legelső hozzászólásáról fogsz értesítést kapni ezekben a kategóriákban." - muted_categories: "Lenémítva" - muted_categories_instructions: "Egyáltalán nem fogsz kapni semmilyen értesítést ennek a kategóriának a témaköreiről, és nem fog megjelenni az olvasatlan oldalon." - no_category_access: "Moderátorként kategóriákra van limitálva a hozzáférésed, mentés le van tiltva." - delete_account: "Fiókom törlése" - delete_account_confirm: "Biztosan végleg törölni akarod a fiókodat? Ez a művelet nem vonható vissza!" - deleted_yourself: "A fiókod törlése sikerült." - delete_yourself_not_allowed: "Kérlek kozlultálj egy staff taggal, ha azt szeretnéd, hogy a fiók törlésre kerüljön." + watched_tags: "Figyelt" + watched_tags_instructions: "Automatikusan figyelni fogja az összes ilyen címkével ellátott témát. Értesítést fog kapni az összes új hozzászólásról és témáról, valamint az új hozzászólások száma megjelenik a téma mellett." + tracked_tags: "Követett" + tracked_tags_instructions: "Automatikusan követni fogja az összes ilyen címkéjű témakört. Az új hozzászólások száma megjelenik a téma mellett." + muted_tags: "Némított" + muted_tags_instructions: "Semmilyen értesítést nem fog kapni a kategória témaköreiről, és a legújabbak között sem fog megjelenni." + watched_categories: "Figyelt" + watched_categories_instructions: "Automatikusan figyelni fogja az összes ebben a kategóriában lévő témát. Értesítést fog kapni az összes új hozzászólásról és témáról, valamint az új hozzászólások száma megjelenik a téma mellett." + tracked_categories: "Követett" + tracked_categories_instructions: "Automatikusan követni fogja az összes ebben a kategóriában lévő témát. Az új hozzászólások száma megjelenik a téma mellett." + watched_first_post_categories: "Első bejegyzés figyelése" + watched_first_post_categories_instructions: "Csak az új témakörök legelső hozzászólásáról fog értesítést kapni ezekben a kategóriákban." + watched_first_post_tags: "Első bejegyzés figyelése" + watched_first_post_tags_instructions: "Csak az új témakörök legelső hozzászólásáról fog értesítést kapni ezekben a címkékben." + muted_categories: "Némított" + muted_categories_instructions: "Semmilyen értesítést nem fog kapni a kategória témaköreiről, és a kategóriák vagy a legújabbak között sem fog megjelenni." + no_category_access: "Moderátorként korlátozott kategória-hozzáférése van, a mentés tiltott." + delete_account: "Saját fiók törlése" + delete_account_confirm: "Biztos, hogy végleg törli a fiókját? Ez a művelet nem vonható vissza." + deleted_yourself: "A fiókja törlése sikeres." + delete_yourself_not_allowed: "Ha törölni akarja a fiókját, akkor vegye fel a kapcsolatot egy stábtaggal." unread_message_count: "Üzenetek" admin_delete: "Törlés" users: "Felhasználók" - muted_users: "Lenémítva" - ignored_users: "Figyelmen kívül hagyva" - tracked_topics_link: "Mutat" + muted_users: "Némított" + ignored_users: "Figyelmen kívül hagyott" + tracked_topics_link: "Megjelenítés" automatically_unpin_topics: "Automatikusan letűzi a témát amikor elérem az alját." apps: "Alkalmazások" revoke_access: "Hozzáférés visszavonása" diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml index 3e4f7589af..1bf54c8ea6 100644 --- a/config/locales/client.hy.yml +++ b/config/locales/client.hy.yml @@ -1435,8 +1435,6 @@ hy: date_time_picker: from: Ում կողմից to: Ում - errors: - to_before_from: "Հաջորդ ամսաթիվը պետք է լինի ավելի մեծ, քան նախորդը:" emoji_picker: filter_placeholder: Փնտրել էմոջի smileys_&_emotion: Սմայլիկներ և Էմոցինաեր diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index b1437fd906..84912b7fc1 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -1113,7 +1113,7 @@ it: title: "Email" primary: "Email principale" secondary: "Email secondaria" - primary_label: "primario" + primary_label: "primaria" unconfirmed_label: "non confermata" resend_label: "Reinvia email di conferma" resending_label: "invio in corso..." @@ -1656,8 +1656,6 @@ it: date_time_picker: from: Da to: A - errors: - to_before_from: "La data di fine deve essere successiva alla data di inizio." emoji_picker: filter_placeholder: Ricerca per emoji smileys_&_emotion: Smileys ed Emotion @@ -2526,7 +2524,7 @@ it: has_replies_count: "%{count}" unknown_user: "(utente sconosciuto / eliminato)" has_likes_title: - one: "Una persona ha messo \"Mi piace\" a questo messaggio" + one: "%{count} persona ha messo \"Mi piace\" a questo messaggio" other: "%{count} persone hanno messo \"Mi piace\" a questo messaggio" has_likes_title_only_you: "hai messo \"Mi piace\" a questo messaggio" has_likes_title_you: @@ -2623,7 +2621,7 @@ it: people: like: one: "ha messo \"Mi piace\"" - other: "ha messo \"Mi piace\"" + other: "hanno messo \"Mi piace\"" read: one: "ha letto" other: "ha letto" @@ -3861,17 +3859,6 @@ it: color_definitions: text: "Definizioni dei colori" title: "Inserisci definizioni di colore personalizzate (solo utenti avanzati)" - placeholder: |2- - - Usa questo foglio di stile per aggiungere colori personalizzati all'elenco delle proprietà personalizzate CSS. - - Esempio: - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } - - E' fortemente raccomandato premettere i nomi delle proprietà per evitare conflitti con i plugin e/o il nucleo. head_tag: text: "" title: "HTML che sarà inserito prima del tag " diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index fc8d9d4704..312b8e0e03 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -387,6 +387,7 @@ ja: email: "Eメール" name: "名前" fields: "広場" + reject_reason: "理由" user_percentage: agreed: other: "%{count}%同意" @@ -470,6 +471,9 @@ ja: other: "%{count} 個の投稿が保留中です。" ok: "OK" example_username: "ユーザ名" + reject_reason: + title: "このユーザーを拒否する理由は何ですか?" + send_email: "拒否メールを送信" user_action: user_posted_topic: "%{user}トピック を作成" you_posted_topic: "あなたトピック を作成" @@ -566,6 +570,10 @@ ja: title: カテゴリ long_title: "カテゴリのデフォルト通知" description: "ユーザーがこのグループに追加されると、ユーザーのカテゴリ通知設定はこれらのデフォルトに設定されます。その後、彼らはそれらを変更することができます。" + watched_categories_instructions: "自動的にカテゴリー内の全てのトピックをウォッチします。グループのメンバーに全ての新規投稿と新規トピックが通知されまた、新規投稿数がトピックの隣に表示されます。" + tracked_categories_instructions: "自動的にカテゴリー内の全てのトピックを追跡します。新規投稿数がトピックの隣に表示されます。" + watching_first_post_categories_instructions: "ユーザーにカテゴリー内の新規トピックに対する最初の投稿が通知されます。" + regular_categories_instructions: "これらのカテゴリがミュートされている場合、グループメンバーのミュートは解除されます。ユーザーがメンションされたり、誰かが返信したりすると、ユーザーに通知されます。" tags: title: タグ long_title: "タグのデフォルト通知" @@ -1569,8 +1577,6 @@ ja: date_time_picker: from: 発信元 to: 宛先 - errors: - to_before_from: "終了日には、開始日よりも後でなければなりません。" emoji_picker: filter_placeholder: 絵文字を探す smileys_&_emotion: スマイリーと感情 diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 09cbc7154b..f361a5d061 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -164,6 +164,7 @@ ko: us_gov_west_1: "AWS GovCloud (미국 서부)" us_west_1: "미국 서부 (N. 캘리포니아)" us_west_2: "미국 서부 (오리건)" + clear_input: "입력 지우기" edit: "이 글의 제목과 카테고리 편집" expand: "확장" not_implemented: "죄송합니다. 아직 사용할 수 없는 기능입니다." @@ -803,6 +804,19 @@ ko: mute_option_title: "이 사용자와 관련된 알림을 받지 않습니다." normal_option: "보통" normal_option_title: "이 사용자가 사용자님에게 댓글을 작성 하거나, 인용하거나, 멘션하면 알림이 전송됩니다." + notification_schedule: + title: "알림 일정" + label: "사용자 지정 알림 일정 사용" + tip: "이 시간 이외에는 자동으로 '방해 금지' 상태가 됩니다." + midnight: "한밤중" + none: "없음" + monday: "월요일" + tuesday: "화요일" + wednesday: "수요일" + thursday: "목요일" + friday: "금요일" + saturday: "토요일" + sunday: "일요일" activity_stream: "활동" preferences: "환경 설정" feature_topic_on_profile: @@ -1599,8 +1613,6 @@ ko: date_time_picker: from: 보내는사람 to: 받는사람 - errors: - to_before_from: "종료 날짜는 시작 날짜보다 이후여야 합니다." emoji_picker: filter_placeholder: 이모티콘 검색 smileys_&_emotion: 웃는 얼굴과 감정 @@ -1881,6 +1893,7 @@ ko: or_search_google: "혹은 구글에서 검색해볼 수도 있습니다." search_google: "대신 구글에서 검색해보세요." search_google_button: "구글" + search_button: "검색" context: user: "@%{username}님의 글 검색" category: "#%{category} 카테고리에서 검색" @@ -3149,6 +3162,7 @@ ko: two_hours: "2시간" tomorrow: "내일까지" custom: "사용자 정의" + set_schedule: "알림 일정 설정" admin_js: type_to_filter: "필터를 입력하세요" admin: @@ -3176,6 +3190,9 @@ ko: installed_version: "설치됨" latest_version: "최신" problems_found: "현재 사이트 설정에 따른 몇 가지 조언" + new_features: + title: "\U0001F381 새로운 기능" + learn_more: "더 보기" last_checked: "마지막으로 확인" refresh_problems: "새로고침" no_problems: "아무런 문제가 발견되지 않았습니다." @@ -3717,17 +3734,6 @@ ko: color_definitions: text: "색상 정의" title: "사용자 색상 정의 입력 (고급 사용자용)" - placeholder: |2- - - 이 스타일시트를 사용하여 사용자 정의 CSS 속성 목록에 사용자 정의 색상을 추가합니다. - - 예: - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } - - 플러그인 및/또는 코어와의 충돌을 피하기 위해 속성 이름 앞에 붙이는 것이 좋습니다. head_tag: text: "" title: " 태그 전에 들어갈 HTML" @@ -4221,6 +4227,9 @@ ko: anonymize_yes: "Yes, anonymize this account" anonymize_failed: "There was a problem anonymizing the account." delete: "사용자 삭제" + delete_posts: + progress: + title: "게시물 삭제 진행률" merge: button: "병합" prompt: @@ -4232,6 +4241,8 @@ ko: target_username_placeholder: "새로운 소유자의 사용자 이름" transfer_and_delete: "전송 및 삭제 @ %{username}" cancel: "취소" + progress: + title: "병합 진행률" confirmation: title: "전송 및 삭제 @ %{username}" description: | @@ -4374,6 +4385,7 @@ ko: go_back: "검색으로 돌아가기" recommended: "다음의 텍스트를 요구에 맞게 편집하는 것을 권장:" show_overriden: "Override 된 설정만 보여주기" + locale: "언어:" more_than_50_results: "50개 이상의 결과가 있습니다. 검색 범위를 좁히십시오." settings: show_overriden: "변경된 것들만 보기" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index b30aca23ef..11be1636a5 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -1650,8 +1650,6 @@ nl: date_time_picker: from: Van to: Aan - errors: - to_before_from: "Tot-datum moet later zijn dan van-datum." emoji_picker: filter_placeholder: Zoeken naar emoji smileys_&_emotion: Smileys en Emotie @@ -3818,17 +3816,6 @@ nl: color_definitions: text: "Kleurdefinities" title: "Aangepaste kleurdefinities invoeren (alleen gevorderde gebruikers)" - placeholder: |2- - - Gebruik dit stylesheet om aangepaste kleuren aan de lijst van aangepaste CSS-eigenschappen toe te voegen. - - Voorbeeld: - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } - - Voorvoegsels bij eigenschapnamen worden ten sterkste aanbevolen om conflicten met plug-ins en/of kern te vermijden. head_tag: text: "" title: "HTML die voor de -tag wordt ingevoegd" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 2e7abd071e..0c58c85e86 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -29,7 +29,7 @@ pl_PL: dates: time: "H:mm" time_with_zone: "HH:mm (z)" - time_short_day: "ddd, GG: mm" + time_short_day: "ddd, HH:mm" timeline_date: "MMM YYYY" long_no_year: "D MMM, HH:mm" long_no_year_no_time: "D MMM" @@ -161,7 +161,7 @@ pl_PL: placeholder: data share: topic_html: 'Temat: %{topicTitle}' - post: "wpis #%{postNumber}" + post: "post #%{postNumber}" close: "zamknij" twitter: "Udostępnij na Twitterze" facebook: "Udostępnij na Facebooku" @@ -233,6 +233,7 @@ pl_PL: us_gov_west_1: "AWS GovCloud (US-West)" us_west_1: "US West (N. California)" us_west_2: "US West (Oregon)" + clear_input: "Wyczyść wprowadzone dane" edit: "edytuj tytuł i kategorię tego tematu" expand: "Rozszerz" not_implemented: "Bardzo nam przykro, ale ta funkcja nie została jeszcze zaimplementowana." @@ -303,7 +304,7 @@ pl_PL: last_30_days: "Ostatnie 30" like_count: "Polubienia" topic_count: "Tematy" - post_count: "Wpisy" + post_count: "Posty" user_count: "Użytkownicy" active_user_count: "Aktywni użytkownicy" contact: "Kontakt" @@ -312,13 +313,13 @@ pl_PL: title: "Zakładka" clear_bookmarks: "Wyczyść zakładki" help: - bookmark: "Kliknij, aby dodać pierwszy wpis tematu do zakładek" + bookmark: "Kliknij, aby dodać pierwszy post tematu do zakładek" unbookmark: "Kliknij, aby usunąć wszystkie zakładki z tego tematu" unbookmark_with_reminder: "Kliknij, aby usunąć wszystkie zakładki i przypomnienia w tym temacie. Masz przypomnienie ustawione na %{reminder_at} dla tego tematu." bookmarks: - created: "Wpis już dodano do zakładek%{name}" + created: "Dodano post do zakładek. %{name}" not_bookmarked: "dodaj do zakładek" - created_with_reminder: "Dodałeś ten wpis do zakładek z przypomnieniem %{date}%{name}" + created_with_reminder: "Dodano post do zakładek z przypomnieniem %{date}. %{name}" remove: "Usuń zakładkę" delete: "Usuń zakładkę" confirm_delete: "Czy na pewno chcesz usunąć tę zakładkę? Przypomnienie również zostanie usunięte." @@ -332,7 +333,7 @@ pl_PL: label: "Usuń automatycznie" never: "Nigdy" when_reminder_sent: "Po wysłaniu przypomnienia" - on_owner_reply: "Po odpowiedzi na ten temat" + on_owner_reply: "Gdy odpowiem w tym temacie" search_placeholder: "Wyszukiwanie zakładek według nazwy, tytułu tematu lub treści postu" search: "Wyszukaj" reminders: @@ -409,12 +410,12 @@ pl_PL: none_found: "Nie znaleziono tematów." title: search: "Wyszukaj temat" - placeholder: "wpisz tytuł tematu, adres URL lub identyfikator tutaj" + placeholder: "wpisz tutaj tytuł tematu, adres URL lub id" choose_message: none_found: "Nie znaleziono wiadomości." title: search: "Wyszukaj wiadomość" - placeholder: "wpisz tutaj tytuł wiadomości, adres URL lub identyfikator" + placeholder: "wpisz tutaj tytuł wiadomości, adres URL lub id" review: order_by: "Segreguj według:" in_reply_to: "w odpowiedzi na" @@ -425,7 +426,7 @@ pl_PL: subtotal: "Suma częściowa" total: "Łącznie" min_score_visibility: "Minimalny wynik dla widoczności" - score_to_hide: "Wynik do ukrycia wpisu" + score_to_hide: "Wynik by ukryć post" take_action_bonus: name: "Podjęto działanie" title: "Gdy członek personelu zdecyduje się na działanie, flaga otrzymuje premię." @@ -545,7 +546,7 @@ pl_PL: conversation: view_full: "zobacz całą rozmowę" scores: - about: "Wynik jest obliczany na podstawie poziomu zaufania raportującego, akceptacji poprzednich flag i priorytetu raportowanego wpisu." + about: "Wynik jest obliczany na podstawie poziomu zaufania raportującego, akceptacji poprzednich flag i priorytetu raportowanego postu." score: "Wynik" date: "Data" type: "Typ" @@ -578,13 +579,13 @@ pl_PL: reviewable_user: title: "Użytkownik" approval: - title: "Wpis wymaga zatwierdzenia" + title: "Post wymaga zatwierdzenia" description: "Twój nowy wpis został umieszczony w kolejce i pojawi się po zatwierdzeniu przez moderatora. Prosimy o cierpliwość." pending_posts: - one: "Masz %{count} oczekujący wpis." - few: "Masz %{count} oczekujące wpisy." - many: "Masz %{count} oczekujących wpisów." - other: "Masz %{count} oczekujących wpisów." + one: "Masz %{count} oczekujący post." + few: "Masz %{count} oczekujące posty." + many: "Masz %{count} oczekujących postów." + other: "Masz %{count} oczekujących postów." ok: "OK" example_username: "użytkownik" reject_reason: @@ -690,7 +691,7 @@ pl_PL: long_title: "Domyślne powiadomienia kategorii" description: "Po dodaniu użytkowników do tej grupy ustawienia powiadomień kategorii zostaną ustawione na wartości domyślne. Potem mogą je zmienić." watched_categories_instructions: "Będziesz automatycznie śledzić wszystkie nowe tematy w tych kategoriach. Będziesz otrzymywać powiadomienie o każdym nowym poście i temacie, a liczba nowych postów będzie wyświetlana obok tytułów na liście tematów." - tracked_categories_instructions: "Będziesz automatycznie śledzić wszystkie tematy w tych kategoriach. Licznik nowych wpisów pojawi się obok tytułu na liście tematów." + tracked_categories_instructions: "Będziesz automatycznie śledzić wszystkie tematy w tych kategoriach. Licznik nowych postów pojawi się obok tytułu na liście tematów." watching_first_post_categories_instructions: "Użytkownicy zostaną powiadomieni o pierwszym postie w każdym nowym temacie w tych kategoriach." regular_categories_instructions: "Jeśli te kategorie są wyciszone, nie zostaną wyciszone dla członków grupy. Użytkownicy zostaną powiadomieni, jeśli zostaną wspomniani lub ktoś na nie odpowie." muted_categories_instructions: "Użytkownicy nie będą powiadamiani o nowych tematach w tych kategoriach i nie będą pojawiać się na stronach kategorii ani najnowszych tematów." @@ -700,7 +701,7 @@ pl_PL: description: "Po dodaniu użytkowników do tej grupy ustawienia powiadomień tagów zostaną ustawione na wartości domyślne. Potem mogą je zmienić." watched_tags_instructions: "Automatycznie obserwuj wszystkie tematy z tymi tagami. Członkowie grupy zostaną powiadomieni o wszystkich nowych postach i tematach, a liczba nowych postów będzie również wyświetlana obok tematu." tracked_tags_instructions: "Automatycznie śledź wszystkie tematy z tymi tagami. Obok tematu pojawi się liczba nowych postów." - watching_first_post_tags_instructions: "Zostaniesz powiadomiony o pierwszym wpisie w każdym nowym temacie oznaczonym tymi tagami." + watching_first_post_tags_instructions: "Zostaniesz powiadomiony o pierwszym poście w każdym nowym temacie oznaczonym tymi tagami." regular_tags_instructions: "Jeśli te tagi są wyciszone, nie zostaną wyciszone dla członków grupy. Użytkownicy zostaną powiadomieni, jeśli zostaną wspomniani lub ktoś na nie odpowie." muted_tags_instructions: "Użytkownicy nie będą powiadamiani o nowych tematach z tymi tagami i nie pojawią się one w najnowszych." logs: @@ -720,7 +721,7 @@ pl_PL: public_admission: "Zezwól wszystkim użytkownikom na dołączanie do tej grupy (widoczność grupy musi być ustawiona na publiczną)" public_exit: "Zezwól wszystkim użytkownikom na opuszczanie tej grupy" empty: - posts: "Członkowie tej grupy nie opublikowali żadnych wpisów." + posts: "Członkowie tej grupy nie napisali żadnych postów." members: "Nie ma użytkowników w tej grupie" requests: "Są prośby o dołączenie do tej grupy." mentions: "Nie ma wspomnień tej grupy" @@ -784,7 +785,7 @@ pl_PL: owner: "Właściciel" forbidden: "Nie możesz oglądać profili użytkowników." topics: "Tematy" - posts: "Wpisów" + posts: "Postów" mentions: "Wzmianki" messages: "Wiadomości" notification_level: "Domyślny poziom powiadomień dla wiadomości grupy." @@ -800,9 +801,9 @@ pl_PL: notifications: watching: title: "Obserwowanie" - description: "Dostaniesz powiadomienie o każdym nowym wpisie w każdej dyskusji, zobaczysz również ilość odpowiedzi." + description: "Dostaniesz powiadomienie o każdym nowym poście w każdej dyskusji, zobaczysz również ilość odpowiedzi." watching_first_post: - title: "Oglądasz pierwszy wpis" + title: "Obserwowanie pierwszego postu" description: "Zostaniesz poinformowany o nowych wiadomościach w tej grupie, ale nie odpowiedziach do wiadomości." tracking: title: "Śledzenie" @@ -850,7 +851,7 @@ pl_PL: save: "Zapisz kolejność" apply_all: "Zastosuj" position: "Pozycja" - posts: "Wpisy" + posts: "Posty" topics: "Tematy" latest: "Aktualne" latest_by: "najnowszy wpis: " @@ -886,7 +887,7 @@ pl_PL: trust_level: "TL" read_time: "czas czytania:" topics_entered: "wprowadzone tematy:" - post_count: "# wpisów" + post_count: "# postów" confirm_delete_other_accounts: "Czy na pewno chcesz usunąć wybrane konta?" powered_by: "używa MaxMindDB" copied: "skopiowano" @@ -902,7 +903,7 @@ pl_PL: button_text: "Pobierz Wszystko" confirm: "Czy na pewno chcesz pobrać swoje posty?" success: "Rozpoczęto eksport: otrzymasz wiadomość, gdy proces zostanie zakończony." - rate_limit_error: "Wpisy mogą być pobierane raz dziennie, spróbuj ponownie jutro." + rate_limit_error: "Posty mogą być pobierane raz dziennie, spróbuj ponownie jutro." new_private_message: "Nowa wiadomość" private_message: "Wiadomość" private_messages: "Wiadomości" @@ -970,7 +971,7 @@ pl_PL: disable: "Wyłącz powiadomienia" enable: "Włącz powiadomienia" each_browser_note: 'Uwaga: musisz zmienić to ustawienie w każdej przeglądarce, której używasz. Wszystkie powiadomienia zostaną wyłączone w trybie „nie przeszkadzać”, niezależnie od tego ustawienia.' - consent_prompt: "Czy chcesz otrzymywać natychmiastowe powiadomienia, gdy ktoś odpowie na twój wpis?" + consent_prompt: "Czy chcesz otrzymywać natychmiastowe powiadomienia, gdy ktoś odpowiada na twoje posty?" dismiss: "Odrzuć" dismiss_notifications: "Odrzuć wszystkie" dismiss_notifications_tooltip: "Oznacz wszystkie powiadomienia jako przeczytane" @@ -1015,22 +1016,22 @@ pl_PL: enabled: "Włącz tryb listy mailingowej" instructions: | To ustawienie nadpisuje podsumowanie aktywności.
    - individual: "Wyślij e-mail dla każdego nowego wpisu" - individual_no_echo: "Wysyłaj emaile dla każdego nowego wpisu oprócz mojego" - many_per_day: "Wyślij mi e-mail dla każdego nowego wpisu (około %{dailyEmailEstimate} na dzień)" - few_per_day: "Wyślij mi e-mail dla każdego nowego wpisu (około 2 dziennie)" + individual: "Wyślij e-mail dla każdego nowego postu" + individual_no_echo: "Wysyłaj e-maile dla każdego nowego postu oprócz mojego" + many_per_day: "Wyślij mi e-mail dla każdego nowego postu (około %{dailyEmailEstimate} na dzień)" + few_per_day: "Wyślij mi e-mail dla każdego nowego postu (około 2 dziennie)" warning: "Tryb listy mailingowej włączony. Ustawienia powiadomień e-mail są zastępowane." tag_settings: "Tagi" watched_tags: "Obserwowane" - watched_tags_instructions: "Będziesz automatycznie śledzić wszystkie nowe tematy z tymi tagami, będziesz otrzymywać powiadomienie o każdym nowym wpisie i temacie, a liczba nieprzeczytanych i nowych wpisów będzie wyświetlana obok tytułów na liście tematów. " + watched_tags_instructions: "Będziesz automatycznie śledzić wszystkie nowe tematy z tymi tagami, będziesz otrzymywać powiadomienie o każdym nowym poście i temacie, a liczba nieprzeczytanych i nowych postów będzie wyświetlana obok tytułów na liście tematów." tracked_tags: "Śledzone" - tracked_tags_instructions: "Będziesz automatycznie śledzić wszystkie nowe tematy z tymi tagami. Licznik nowych wpisów pojawi się obok tytułu na liście tematów." + tracked_tags_instructions: "Będziesz automatycznie śledzić wszystkie nowe tematy z tymi tagami. Licznik nowych postów pojawi się obok tytułu na liście tematów." muted_tags: "Wyciszone" muted_tags_instructions: "Nie będziesz powiadamiany o niczym dotyczącym nowych tematów z tymi tagami i nie pojawią się na liście aktualnych." watched_categories: "Obserwowane" - watched_categories_instructions: "Będziesz automatycznie śledzić wszystkie nowe tematy w tych kategoriach. Będziesz otrzymywać powiadomienie o każdym nowym wpisie i temacie, a liczba nowych wpisów będzie wyświetlana obok tytułów na liście tematów. " + watched_categories_instructions: "Będziesz automatycznie śledzić wszystkie nowe tematy w tych kategoriach. Będziesz otrzymywać powiadomienie o każdym nowym poście i temacie, a liczba nowych postów będzie wyświetlana obok tytułów na liście tematów." tracked_categories: "Śledzone" - tracked_categories_instructions: "Będziesz automatycznie śledzić wszystkie tematy w tych kategoriach. Licznik nowych wpisów pojawi się obok tytułu na liście tematów." + tracked_categories_instructions: "Będziesz automatycznie śledzić wszystkie tematy w tych kategoriach. Licznik nowych postów pojawi się obok tytułu na liście tematów." watched_first_post_categories: "Oglądasz pierwszy wpis" watched_first_post_categories_instructions: "Zostaniesz powiadomiony o pierwszym wpisie w każdym nowym temacie w tych kategoriach." watched_first_post_tags: "Oglądasz pierwszy wpis" @@ -1539,7 +1540,7 @@ pl_PL: close: "zamknij" dismiss_error: "Odrzuć błąd" close: "Zamknij" - assets_changed_confirm: "Serwis został zmieniony, czy pozwolisz na przeładowanie strony w celu aktualizacji do najnowszej wersji?" + assets_changed_confirm: "Serwis został zaktualizowany. Odświeżyć stronę aby otrzymać najnowszą wersję?" logout: "Nastąpiło wylogowanie." refresh: "Odśwież" home: "Strona główna" @@ -1792,8 +1793,6 @@ pl_PL: date_time_picker: from: Od to: Do - errors: - to_before_from: "Do daty musi być późniejsza niż data." emoji_picker: filter_placeholder: Szukaj emoji smileys_&_emotion: Buźki i emocje @@ -2321,8 +2320,8 @@ pl_PL: browse_all_tags: Przeglądaj wszystkie tagi view_latest_topics: pokaż aktualne tematy suggest_create_topic: rozpocząć nową rozmowę? - jump_reply_up: przeskocz do wcześniejszej odpowiedzi - jump_reply_down: przeskocz do późniejszej odpowiedzi + jump_reply_up: skocz do wcześniejszej odpowiedzi + jump_reply_down: skocz do późniejszej odpowiedzi deleted: "Temat został usunięty" slow_mode_update: title: "Tryb powolny" @@ -2419,7 +2418,7 @@ pl_PL: jump_prompt: "skocz do..." jump_prompt_of: "%{count} wpisów" jump_prompt_long: "Skocz do..." - jump_bottom_with_number: "przeskocz do wpisu %{post_number}" + jump_bottom_with_number: "skocz do postu %{post_number}" jump_prompt_to_date: "do daty" jump_prompt_or: "lub" total: w sumie wpisów @@ -3604,6 +3603,10 @@ pl_PL: installed_version: "Zainstalowana" latest_version: "Najnowsza" problems_found: "Porada bazująca na twoich aktualnych ustawieniach" + new_features: + title: "\U0001F381 Nowe funkcje" + dismiss: "Odrzuć" + learn_more: "Dowiedz się więcej" last_checked: "Ostatnio sprawdzana" refresh_problems: "Odśwież" no_problems: "Nie znaleziono problemów." @@ -3736,6 +3739,11 @@ pl_PL: group_members: "Członkowie grupy" delete: "Usuń" delete_confirm: "Usunąć tę grupę?" + delete_with_messages_confirm: + one: "Usunięcie tej grupy spowoduje, że %{count} wiadomość zostanie osierocona, a członkowie grupy nie będą już mieli do niej dostępu.

    Jesteś pewien?" + few: "Usunięcie tej grupy spowoduje, że %{count} wiadomości zostanie osieroconych, a członkowie grupy nie będą już mieli do nich dostępu.

    Jesteś pewien?" + many: "Usunięcie tej grupy spowoduje, że %{count} wiadomości zostanie osieroconych, a członkowie grupy nie będą już mieli do nich dostępu.

    Jesteś pewien?" + other: "Usunięcie tej grupy spowoduje, że %{count} wiadomości zostanie osieroconych, a członkowie grupy nie będą już mieli do nich dostępu.

    Jesteś pewien?" delete_failed: "Nie można usunąć grupy. Jeżeli jest to grupa automatyczna, nie może zostać zniszczona." delete_owner_confirm: "Usunąć status właściciela dla '%{username}'?" add: "Dodaj" @@ -3968,7 +3976,7 @@ pl_PL: rollback: label: "Wycofaj" title: "Wycofaj bazę danych do poprzedniego poprawnego stanu" - confirm: "Czy jesteś pewien, że chcesz przywrócić bazę danych do ostatniej działającej wersji ?" + confirm: "Czy jesteś pewien, że chcesz przywrócić bazę danych do ostatniej działającej wersji?" location: local: "Lokalne miejsce" s3: "S3" @@ -4121,13 +4129,13 @@ pl_PL: minimum: "Wymaga wersji %{version} Discourse'a lub wyższej." maximum: "Wymaga wersji %{version} Discourse'a lub niższej." component_of: "Składnik:" - update_to_latest: "Aktualizuj do najnowszego" + update_to_latest: "Zaktualizuj do najnowszej wersji" check_for_updates: "Sprawdź dostępność aktualizacji" updating: "Trwa proces aktualizacji..." up_to_date: "Motyw jest aktualny. Ostatnio sprawdzano:" has_overwritten_history: "Bieżąca wersja motywu już nie istnieje, ponieważ historia Git została nadpisana przez wymuszony push." add: "Dodaj" - theme_settings: "Ustawienia tematu" + theme_settings: "Ustawienia motywu" no_settings: "Ten temat nie ma żanych ustawień" theme_translations: "Tłumaczenia tematu" empty: "Brak elementu" @@ -4159,15 +4167,13 @@ pl_PL: title: "Określ własne kolory (tylko dla zaawansowanych użytkowników)" placeholder: |2- - Użyj tego arkusza stylów, aby dodać kolory do listy niestandardowych właściwości CSS. + Użyj tego arkusza stylów, aby dodać własne kolory do listy niestandardowych właściwości CSS. - Na przykład: + Przykład: - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } + %{example} - Zdecydowanie zaleca się dodawanie przedrostków do nazw właściwości, aby uniknąć konfliktów z zainstalowanymi wtyczkami lub silnikiem forum. + Zdecydowanie zalecamy dodawanie przedrostków do nazw zmiennych, aby uniknąć konfliktów z wtyczkami i/lub silnikiem forum. head_tag: text: "" title: "HTML, który zostanie umieszczony przed tagiem " @@ -4835,6 +4841,7 @@ pl_PL: recommended: "Zalecamy zmianę poniższego tekstu, aby lepiej odpowiadał Twoim potrzebom:" show_overriden: "Pokaż tylko nadpisane" locale: "Język:" + fallback_locale_warning: "Edytujesz język oparty na %{fallback}. Użytkownicy, którzy wybiorą %{fallback} jako język interfejsu, nie zobaczą Twoich zmian." more_than_50_results: "Istnieje ponad 50 wyników. Zawęź wyszukiwanie." settings: show_overriden: "Pokaż tylko nadpisane" @@ -4989,7 +4996,7 @@ pl_PL: group: "Grupa" image: "Grafika" alt: "podgląd niestandardowego emoji" - delete_confirm: "Jesteś pewny(-a), że chcesz usunąć emoji :%{name}: ?" + delete_confirm: "Jesteś pewny(-a), że chcesz usunąć emoji :%{name}:?" embedding: get_started: "Jeśli chcesz osadzić Discourse na innej stronie, rozpocznij podając jej host." confirm_delete: "Czy na pewno chcesz usunąć ten host?" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 3b77d8b856..338875e431 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -1544,8 +1544,6 @@ pt_BR: date_time_picker: from: De to: Para - errors: - to_before_from: "Até a data deve ser posterior a partir da data." emoji_picker: filter_placeholder: Pesquisar por emoji smileys_&_emotion: Smileys e Emotion @@ -2031,7 +2029,7 @@ pt_BR: title: "Excluir respostas automaticamente" status_update_notice: auto_open: "Este tópico abrirá automaticamente em %{timeLeft}." - auto_close: "Este tópico fechará automaticamente em %{timeLeft}." + auto_close: "Este tópico fechará automaticamente %{timeLeft}." auto_publish_to_category: "Este tópico será publicado em #%{categoryName} %{timeLeft}." auto_close_based_on_last_post: "Este tópico fechará %{duration} após a última resposta." auto_delete: "Este tópico será automaticamente excluído %{timeLeft}." diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index a8a2a58382..1e1ce420a0 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -233,6 +233,7 @@ ru: us_gov_west_1: "AWS GovCloud (США-Запад)" us_west_1: "US West (Сев. Калифорния)" us_west_2: "US West (Орегон)" + clear_input: "Очистить" edit: "Отредактировать название и раздел темы" expand: "Развернуть" not_implemented: "Извините, эта функция ещё не реализована!" @@ -1793,8 +1794,6 @@ ru: date_time_picker: from: От to: Кому - errors: - to_before_from: "Конечная дата должна быть больше начальной." emoji_picker: filter_placeholder: Поиск смайла smileys_&_emotion: Смайлики и эмоции @@ -3605,6 +3604,10 @@ ru: installed_version: "Установленная версия" latest_version: "Последняя версия" problems_found: "Несколько советов по вашим текущим настройкам сайта" + new_features: + title: "\U0001F381 Новые возможности" + dismiss: "Отклонить" + learn_more: "Подробнее" last_checked: "Последняя проверка" refresh_problems: "Обновить" no_problems: "Проблем не обнаружено." @@ -3737,6 +3740,11 @@ ru: group_members: "Участники группы" delete: "Удалить" delete_confirm: "Удалить эту группу?" + delete_with_messages_confirm: + one: "Удаление этой группы приведет к тому, что члены группы больше не будут иметь доступа к %{count} сообщению.

    Продолжить?" + few: "Удаление этой группы приведет к тому, что члены группы больше не будут иметь доступа к %{count} сообщениям.

    Продолжить?" + many: "Удаление этой группы приведет к тому, что члены группы больше не будут иметь доступа к %{count} сообщениям.

    Продолжить?" + other: "Удаление этой группы приведет к тому, что члены группы больше не будут иметь доступа к %{count} сообщениям.

    Продолжить?" delete_failed: "Невозможно удалить группу. Если группа была создана автоматически, то она не может быть удалена." delete_owner_confirm: "Отозвать права владельца у пользователя '%{username}'?" add: "Добавить" @@ -4158,7 +4166,7 @@ ru: color_definitions: text: "Определение цветовой схемы" title: "Укажите собственные цвета (только для опытных пользователей)" - placeholder: "\nИспользуйте эту таблицу стилей для добавления цветов в список пользовательских свойств CSS.\n\nПример: \n\n:root { --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};\n}\n\nНастоятельно рекомендуется добавлять префиксы к именам свойств, чтобы избежать конфликтов с плагинами и / или движком форума." + placeholder: "\nИспользуйте эту таблицу стилей для добавления цветов в список пользовательских свойств CSS.\n\nПример: \n\n%{example}\n\nНастоятельно рекомендуется добавлять префиксы к именам свойств, чтобы избежать конфликтов с плагинами и / или движком форума." head_tag: text: "" title: "HTML для размещения перед тегом " @@ -4655,6 +4663,9 @@ ru: anonymize_yes: "Да, анонимизировать эту учётную запись" anonymize_failed: "Не удалось анонимизировать учётную запись." delete: "Удалить пользователя" + delete_posts: + progress: + title: "Удаление сообщений" merge: button: "Объединить пользователей" prompt: @@ -4666,6 +4677,8 @@ ru: target_username_placeholder: "Псевдоним нового владельца" transfer_and_delete: "Объединить контент и удалить пользователя @%{username}" cancel: "Отмена" + progress: + title: "Объединение пользователей" confirmation: title: "Объединить контент и удалить пользователя @%{username}" description: | @@ -4821,6 +4834,7 @@ ru: recommended: "Мы рекомендуем изменить текст этих шаблонов под ваши требования:" show_overriden: "Показывать только изменённые значения" locale: "Язык:" + fallback_locale_warning: "Вы редактируете язык на основе %{fallback}. Пользователи, выбравшие %{fallback} в качестве языка интерфейса, не увидят ваши изменения." more_than_50_results: "Найдено более 50 результатов. Пожалуйста, уточните параметры поиска." settings: show_overriden: "Показывать только изменённые значения" diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index e7c16d7e0b..f19f37074d 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -1664,8 +1664,6 @@ sl: date_time_picker: from: Od to: Do - errors: - to_before_from: "Datum Od mora biti starejši kot Do datum" emoji_picker: filter_placeholder: Išči emoji smileys_&_emotion: Smeški in emotikoni diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index dd06195073..69703eabd1 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -187,6 +187,7 @@ sv: us_gov_west_1: "AWS GovCloud (Västra USA)" us_west_1: "Västra USA (N. Kalifornien)" us_west_2: "Västra USA (Oregon)" + clear_input: "Rensa inmatning" edit: "redigera rubrik och kategori för det här ämnet" expand: "Utvidga" not_implemented: "Denna funktion har tyvärr inte implementerats än!" @@ -1673,8 +1674,6 @@ sv: date_time_picker: from: Från to: Till - errors: - to_before_from: "Till datum måste vara senare än Från datum." emoji_picker: filter_placeholder: Sök efter emoji smileys_&_emotion: Smileys och känslor @@ -3335,6 +3334,10 @@ sv: installed_version: "Installerad" latest_version: "Senaste" problems_found: "Några förslag baserat på dina nuvarande webbplatsinställningar" + new_features: + title: "\U0001F381 Nya funktioner" + dismiss: "Avfärda" + learn_more: "Läs mer" last_checked: "Senast kollad" refresh_problems: "Uppdatera" no_problems: "Inga problem upptäcktes." @@ -3465,6 +3468,9 @@ sv: group_members: "Gruppmedlemmar" delete: "Radera" delete_confirm: "Ta bort den här gruppen?" + delete_with_messages_confirm: + one: "Om du raderar den här gruppen blir %{count} meddelande ägarlöst, och gruppmedlemmarna får inte längre tillgång till det.

    Är du säker?" + other: "Om du raderar den här gruppen blir %{count} meddelanden ägarlösa, och gruppmedlemmarna får inte längre tillgång till dem.

    Är du säker?" delete_failed: "Det går inte att ta bort grupp. Om det här är en automatisk grupp så kan den inte raderas." delete_owner_confirm: "Vill du ta bort användarprivilegier för '%{username}'?" add: "Lägg till" @@ -3886,11 +3892,9 @@ sv: Exempel: - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary) - } + %{example} - Användning av prefix för egenskapsnamnen rekommenderas starkt för att undvika konflikter med tillägg och/eller grundläggande programvara. + Användning av prefix för egenskapsnamnen rekommenderas starkt för att undvika konflikter mellan tillägg och/eller grundläggande program. head_tag: text: "" title: "HTML som kommer att sättas in före taggen" @@ -4548,6 +4552,7 @@ sv: recommended: "Vi rekommenderar att du anpassar följande texter:" show_overriden: "Visa bara överskrivna" locale: "Språk:" + fallback_locale_warning: "Du redigerar ett språk baserat på %{fallback}. Användare som väljer %{fallback} som sitt gränssnittsspråk kommer inte att se dina ändringar." more_than_50_results: "Det blev fler än 50 resultat. Förfina din sökning." settings: show_overriden: "Visa bara överskrivna" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 6449a5c7d3..efe1ca8d3b 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -768,6 +768,19 @@ tr_TR: mute_option_title: "Bir daha bu kullanıcıyla ilgili bildirim almayacaksın." normal_option: "Normal" normal_option_title: "Bu kullanıcı sizi yanıtlarsa, alıntılarsa ya da sizden bahsederse bildirim alacaksınız." + notification_schedule: + title: "Bildirim Zamanlaması" + label: "Özel bildirim zamanlamasını etkinleştir" + tip: "Bu saatler dışında otomatik olarak 'rahatsız etmeyin' durumuna alınacaksınız." + midnight: "Gece yarısı" + none: "Yok" + monday: "Pazartesi" + tuesday: "Salı" + wednesday: "Çarşamba" + thursday: "Perşembe" + friday: "Cuma" + saturday: "Cumartesi" + sunday: "Pazar" activity_stream: "Aktivite" preferences: "Tercihler" feature_topic_on_profile: @@ -1452,8 +1465,6 @@ tr_TR: date_time_picker: from: Kimden to: Kime - errors: - to_before_from: "Bugüne kadar olan tarihten itibaren olmalıdır." emoji_picker: filter_placeholder: Emoji ara smileys_&_emotion: Emojiler ve Duygular @@ -2874,6 +2885,8 @@ tr_TR: dismiss: "Yoksay" safe_mode: enabled: "Güvenli mod etkin, çıkmak için bu tarayıcı penceresini kapat" + do_not_disturb: + set_schedule: "Bildirim zamanlaması ayarla" admin_js: type_to_filter: "filtrelemek için yaz..." admin: diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 6c2b1445d8..661ca94bca 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -1779,8 +1779,6 @@ uk: date_time_picker: from: Від to: До - errors: - to_before_from: "На сьогоднішній день має бути пізніше, ніж з дати." emoji_picker: filter_placeholder: Шукати emoji smileys_&_emotion: Смайли та емоції @@ -4142,17 +4140,6 @@ uk: color_definitions: text: "Визначення кольорів" title: "Введіть власні визначення кольорів (лише для досвідчених користувачів)" - placeholder: |2- - - Використовуйте цю таблицю стилів, щоб додати власні кольори до списку власних властивостей CSS. - - Приклад: - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } - - Настійно рекомендується вводити префікси до назв властивостей, щоб уникнути конфліктів з плагінами та/або ядром. head_tag: text: "" title: "HTML-код, який буде вставлений перед міткою " diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 57d40dab23..b19e8f458f 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -1378,8 +1378,6 @@ ur: date_time_picker: from: سے to: کیلئے - errors: - to_before_from: "'کب تک' کی تاریخ 'کب سے' کی تاریخ کے بعد ہونا ضروری ہے۔" emoji_picker: filter_placeholder: اِیمَوجی تلاش کریں smileys_&_emotion: سمائیلیاں اور جذبات diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 674924f945..326df2cc80 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -1594,8 +1594,6 @@ vi: date_time_picker: from: Từ to: Tới - errors: - to_before_from: "\"Đến ngày\" phải sau \"từ ngày\"." emoji_picker: filter_placeholder: Tìm kiếm emoji smileys_&_emotion: Biểu tượng mặt cười và cảm xúc @@ -3704,17 +3702,6 @@ vi: color_definitions: text: "Màu sắc" title: "Nhập màu tùy chỉnh (chỉ dành cho người dùng nâng cao)" - placeholder: |2- - - Sử dụng biểu định kiểu này để thêm màu tùy chỉnh vào danh sách các thuộc tính tùy chỉnh CSS. - - Ví dụ: - - : root { - --mytheme-tertiary-or-quaternary: # {dark-light-select ($tertiary, $quaternary)}; - } - - nên đặt tiền tố tên thuộc tính để tránh xung đột với các plugin và / hoặc lõi. head_tag: text: "" title: "HTML sẻ thêm trước thẻ " diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 2cd220cf71..83edb4d91c 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -164,6 +164,7 @@ zh_CN: us_gov_west_1: "AWS 政府云(US-West)" us_west_1: "美国西部(N. California)" us_west_2: "美国西部(Oregon)" + clear_input: "清除输入" edit: "编辑该主题的标题和分类" expand: "展开" not_implemented: "非常抱歉,这个功能仍在开发中!" @@ -803,6 +804,20 @@ zh_CN: mute_option_title: "你不会收到任何关于此用户的通知" normal_option: "普通" normal_option_title: "如果用户回复、引用或提到你,你将会收到消息。" + notification_schedule: + title: "定时通知" + label: "启用自定义定时通知" + tip: "在这些时间之外,你将自动进入 “请勿打扰” 状态。" + midnight: "午夜" + none: "无" + monday: "星期一" + tuesday: "星期二" + wednesday: "星期三" + thursday: "星期四" + friday: "星期五" + saturday: "星期六" + sunday: "星期日" + to: "到" activity_stream: "活动" preferences: "设置" feature_topic_on_profile: @@ -1599,8 +1614,6 @@ zh_CN: date_time_picker: from: 从 to: 发至 - errors: - to_before_from: "截至日期必须晚于开始日期。" emoji_picker: filter_placeholder: 查找表情符号 smileys_&_emotion: 笑脸与情感 @@ -1881,6 +1894,7 @@ zh_CN: or_search_google: "或者尝试使用Google进行搜索:" search_google: "尝试使用Google进行搜索:" search_google_button: "Google" + search_button: "搜索" context: user: "搜索 @%{username} 的帖子" category: "搜索 #%{category} 分类" @@ -3157,6 +3171,7 @@ zh_CN: two_hours: "2 小时" tomorrow: "直到明天" custom: "自定义" + set_schedule: "设定一个定时通知" admin_js: type_to_filter: "输入以筛选…" admin: @@ -3184,6 +3199,10 @@ zh_CN: installed_version: "已安装" latest_version: "最新版本" problems_found: "根据您当前的站点设置提供一些建议" + new_features: + title: "\U0001F381 新功能" + dismiss: "忽略" + learn_more: "了解更多" last_checked: "上次检查" refresh_problems: "刷新" no_problems: "找不到问题." @@ -3313,6 +3332,8 @@ zh_CN: group_members: "群组成员" delete: "删除" delete_confirm: "删除这个群组吗?" + delete_with_messages_confirm: + other: "删除此群组将导致 %{count} 条消息被孤立,群组成员将无法再访问它们。

    你确定吗?" delete_failed: "无法删除群组。如果该群组是自动生成的,则不可删除。" delete_owner_confirm: "移除“%{username}”的权限?" add: "添加" @@ -3727,14 +3748,11 @@ zh_CN: title: "输入自定义颜色配置(仅限高级用户)" placeholder: |2- - 使用这个样式表将自定义颜色添加到CSS自定义属性。 + 使用这个样式表将自定义的颜色添加到CSS自定义属性列表。 例如: - - :root { - --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)}; - } + %{example} 强烈建议使用前缀为属性命名以避免与插件或核心程序冲突。 head_tag: @@ -4389,6 +4407,7 @@ zh_CN: recommended: "我们建议自定义以下文本以符合你的需求:" show_overriden: "只显示修改过的" locale: "语言:" + fallback_locale_warning: "您正在基于 %{fallback}编辑语言。选择 %{fallback} 作为界面语言的用户将看不到你的更改。" more_than_50_results: "有超过50个结果。请优化您的搜索。" settings: show_overriden: "只显示被覆盖" diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index d1776194a9..5715e650a4 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -379,6 +379,7 @@ de: bookmarks: errors: already_bookmarked_post: "Du kannst denselben Beitrag nicht zweimal zu deinen Lesezeichen hinzufügen." + too_many: "Sorry, du hast zu viele Lesezeichen, besuche %{user_bookmarks_url} , um einige zu entfernen." cannot_set_past_reminder: "Sie können in der Vergangenheit keine Lesezeichenerinnerung festlegen." cannot_set_reminder_in_distant_future: "Sie können eine Lesezeichenerinnerung nicht länger als 10 Jahre in der Zukunft festlegen." time_must_be_provided: "die Zeit muss für alle Erinnerungen angegeben werden" @@ -1653,6 +1654,7 @@ de: avatar_sizes: "Liste der Größe von automatisch generierten Profilbildern." external_system_avatars_enabled: "Benutze einen externen Avatar Service" external_system_avatars_url: "URL des externen Profilbild-Dienstes. Erlaubte Platzhalter sind {username} {first_letter} {color} {size}" + use_site_small_logo_as_system_avatar: "Verwende das kleine Logo der Website anstelle des Avatars des Systembenutzers. Das Logo muss vorhanden sein." restrict_letter_avatar_colors: "Eine Liste 6stelliger hexadezimaler Farbwerte für den Letter Avatar Hintergrund." selectable_avatars_enabled: "Forciere Benutzer, ein Avatar aus der Liste auszuwählen." selectable_avatars: "Liste von Avataren, aus der Benutzer wählen können." @@ -3440,6 +3442,8 @@ de: Genieße deinen Aufenthalt! signup_after_reject: + title: "Anmelden nach Ablehnung" + subject_template: "Du wurdest von %{site_name} abgelehnt" text_body_template: | Ein Team-Mitglied hat dein Konto bei %{site_name} abgelehnt. diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 8665fbe5e2..f3ba29d415 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -260,6 +260,7 @@ es: email_template_cant_be_modified: "Esta plantilla de correo electrónico no se puede modificar" invalid_whisper_access: "O los susurros no están habilitados o no tienes permitido crear susurros" not_in_group: + title_topic: "Necesitas solicitar formar parte del grupo «%{group}» para ver este tema" title_category: "Debes estar en grupo para ver esta categoría." request_membership: "Solicitar membresía" join_group: "Unirse al grupo" @@ -375,6 +376,7 @@ es: bookmarks: errors: already_bookmarked_post: "No puedes guardar la misma publicación en marcadores dos veces." + too_many: "Lo sentimos, pero tienes demasiados marcadores. Visita %{user_bookmarks_url} para quitar algunos." cannot_set_past_reminder: "No puedes establecer un recordatorio de marcador en el pasado." cannot_set_reminder_in_distant_future: "No puedes establecer un recordatorio de marcador más de 10 años en el futuro." time_must_be_provided: "se debe indicar el tiempo para todos los recordatorios" @@ -424,6 +426,26 @@ es: until_posts: one: "%{count} post" other: "%{count} publicaciones" + "new-topic": | + Te damos la bienvenida a %{site_name}. **¡Gracias por empezar un nuevo tema!** + + - ¿Suena el título interesante si lo lees en alto? ¿Es un buen resumen del contenido? + + - ¿A quién le podría interesar? ¿Por qué importa? ¿Qué tipo de respuestas buscas? + + - Incluye palabras comúnmente utilizadas en tu tema para que los demás puedan **encontrarlo**. Para agrupar el tema con otros relacionados, selecciona una categoría (o etiqueta). + + Para más información [consulta nuestra nuestra guía](%{base_path}/guidelines). Este panel solo aparecera para tus primeros %{education_posts_text} mensajes. + "new-reply": | + Te damos la bienvenida a %{site_name}. **¡Gracias por contribuir!** + + - Sé amable con las demás personas de la comunidad. + + - ¿Aporta tu respuesta a la conversación? + + - Las críticas constructivas son bienvenidas, pero recuerda criticar las *ideas*, y no a las personas. + + Para más información [consulta nuestra guía](%{base_path}/guidelines). Este panel solo aparecerá para tus primeros %{education_posts_text} mensajes. avatar: | ### ¿Que tal si agregas una imagen para tu cuenta? @@ -931,7 +953,9 @@ es: mute_topic: "Silenciar todas las notificaciones de este tema, %{link}" unwatch_category: "Dejar de vigilar todos los temas de %{category}" mailing_list_mode: "Desactivar el modo lista de correo" + all: "No enviarme ningún correo desde %{sitename}" different_user_description: "Actualmente has iniciado sesión con un usuario diferente al que te hemos enviado el correo electrónico. Por favor, cierra sesión o entra en modo anónimo antes de intentarlo de nuevo." + not_found_description: "Lo sentimos, pero no hemos encontrado este enlace de baja. ¿Es posible que el enlace en el correo sea antiguo y haya expirado?" log_out: "Cerrar sesión" submit: "Guardar preferencias" digest_frequency: diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 151d8b51ac..cc50d5031f 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -1528,7 +1528,7 @@ fr: email_token_valid_hours: "Les jetons (tokens) de Mot de passe oublié / Activation de comptes sont valides (n) jours." enable_badges: "Activer le système de badges" enable_whispers: "Autoriser les communications privées entre responsables au sein d'un sujet." - allow_index_in_robots_txt: "Spécifiez dans le fichier robots.txt que ce site peut être indexé par les moteurs de recherche web. Dans des cas exceptionnels, vous pouvez remplacer définitivement le fichier robots.txt." + allow_index_in_robots_txt: "Indiquer dans le fichier robots.txt que ce site peut être indexé par les moteurs de recherche web. Dans des cas exceptionnels, vous pouvez remplacer définitivement le fichier robots.txt." auto_approve_email_domains: "Les utilisateurs dont le domaine de l'adresse courriel appartient à cette liste seront automatiquement approuvés." hide_email_address_taken: "Ne pas informer les utilisateurs qu'un compte existe avec un courriel donné lors de l'inscription ou à partir du formulaire d'oubli du mot de passe." log_out_strict: "Lors de la déconnexion, déconnecter TOUTES les sessions de l'utilisateur sur tous les appareils" diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml index b5b19d7350..33261263ec 100644 --- a/config/locales/server.gl.yml +++ b/config/locales/server.gl.yml @@ -379,6 +379,7 @@ gl: bookmarks: errors: already_bookmarked_post: "Non podes gardar a mesma publicación nos marcadores dúas veces." + too_many: "Desculpe, ten marcadores de máis, revise %{user_bookmarks_url} para retirar algúns." cannot_set_past_reminder: "Non podes establecer un recordatorio de marcador no pasado." cannot_set_reminder_in_distant_future: "Non podes establecer un recordatorio de marcador máis de 10 anos cara ao futuro." time_must_be_provided: "o período debe ser facilitado para todos os recordatorios" @@ -1653,6 +1654,7 @@ gl: avatar_sizes: "Listaxe de tamaños dos avatares xerados automaticamente." external_system_avatars_enabled: "Utilizar un servizo externo de avatares." external_system_avatars_url: "Enderezo URL do servizo externo para os avatares. As substitucións permitidas son {username} {first_letter} {color} {size}" + use_site_small_logo_as_system_avatar: "Utilice o pequeno logotipo do sitio no canto do avatar de usuario do sistema. Require que estea presente o logotipo." restrict_letter_avatar_colors: "Unha listaxe de valores hexadecimais das cores permitidas para os fondos dos avatares de letras." selectable_avatars_enabled: "Obrigar os usuarios a elixiren un avatar da listaxe." selectable_avatars: "Listaxe de avatares que os usuarios poden elixir." @@ -3443,6 +3445,13 @@ gl: Cremos nunha comunidade cun [comportamento civilizado](%{base_url}/directrices) en todo momento. Desfruta a túa estancia! + signup_after_reject: + title: "Rexistro despois do rexeitamento" + subject_template: "Resultou rexeitado en %{site_name}" + text_body_template: | + Un membro do grupo rexeitou a súa conta en %{site_name}. + + %{reject_reason} signup: title: "Rexistrarse" subject_template: "[%{email_prefix}] Confirma a túa nova conta" diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index d3d4b66c0e..eb97a3cf23 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -3732,10 +3732,10 @@ it: label: "Argomento di Benvenuto" description: "

    Come descriveresti la tua comunità ad un estraneo su un ascensore in circa 1 minuto?

    • Per chi sono queste discussioni?
    • Cosa posso trovare qui?
    • Perché dovrei visitare il sito?

    Il tuo argomento di benvenuto è la prima cosa che i nuovi arrivati leggeranno. Pensalo come al tuo paragrafo 'discorso da ascensore' o 'dichiarazione di intenti'.

    " one_paragraph: "Restringi il tuo messaggio di benvenuto a un paragrafo." - extra_description: "In caso d'incertezza, puoi saltare questo passaggio e modificare il tuo argomento di benvenuto più tardi." + extra_description: "Se non si è sicuri, è possibile saltare questo passaggio e modificare l'argomento di benvenuto in un secondo momento." privacy: title: "Accesso" - description: "

    La tua comunità è aperta a tutti, o è limitata da adesione, invito o approvazione? Se preferisci, puoi impostare le cose in privato, quindi passare a pubblico più avanti.

    " + description: "

    La tua comunità è aperta a tutti o è limitata da appartenenza, invito o approvazione? Se si preferisce, puoi configurare le cose in privato, quindi passare a pubblico in seguito.

    " fields: privacy: choices: @@ -3751,7 +3751,7 @@ it: open: label: "Gli utenti possono registrarsi autonomamente." invite_only: - label: "Gli utenti devono essere invitati da utenti o dallo staff prima che possano registrarsi." + label: "Gli utenti devono essere invitati da utenti attendibili o dallo staff prima di potersi registrare." must_approve: label: "Gli utenti possono registrarsi autonomamente, ma devono essere approvati dallo staff." contact: @@ -3760,7 +3760,7 @@ it: contact_email: label: "Email" placeholder: "nome@esempio.com" - description: "Indirizzo email della persona o del gruppo responsabile di questa comunità. Utilizzato per notifiche critiche quali segnalazioni non gestite, aggiornamenti sulla protezione e sulla tua pagina informazioni per un contatto urgente." + description: "Indirizzo email della persona o del gruppo responsabile di questa comunità. Utilizzato per notifiche critiche come segnalazioni non gestite, aggiornamenti di sicurezza e sulla tua pagina Informazioni per un contatto urgente." contact_url: label: "Sito Web" placeholder: "https://www.example.com/contact-us" @@ -3806,10 +3806,10 @@ it: fields: favicon: label: "Icona del Browser" - description: "Icona usata per rappresentare il tuo sito sul browser web, che si veda bene in piccole dimensioni. Le estensioni raccomandate sono PNG e JPG. Useremo il logo quadrato come predefinito." + description: "Icona usata per rappresentare il tuo sito sul browser web, che si veda bene in piccole dimensioni. Le estensioni di immagini consigliate sono PNG e JPG. Per impostazione predefinita useremo il logo quadrato." large_icon: label: "Icona Grande" - description: "Icona usata per rappresentare il tuo sito sul dispositivi moderni, che si veda bene in grandi dimensioni. Idealmente più grande di 512x512. Useremo il logo quadrato come predefinito." + description: "Icona usata per rappresentare il tuo sito sul dispositivi moderni, che si veda bene in grandi dimensioni. Idealmente più grande di 512 x 512. Per impostazione predefinita useremo il logo quadrato." homepage: description: "Ti consigliamo di mostrare gli ultimi argomenti nella tua homepage, ma puoi anche visualizzare le categorie (gruppi di argomenti) sulla homepage, se preferisci." title: "Homepage" @@ -3840,7 +3840,7 @@ it: finished: title: "Il tuo Discourse è Pronto!" description: | -

    Se vorrai cambiare queste impostazioni, esegui nuovamente questa procedura guidata in qualsiasi momento, o visita la sezione admin; lo trovi accanto all'icona della chiave chiave nel menu del sito.

    +

    Se vuoi cambiare queste impostazioni, riesegui questa procedura guidata in qualsiasi momento, o visita il pannello amministrativo; lo trovi accanto all'icona della chiave inglese nel menu del sito.

    È facile personalizzare il tuo Discourse ancora di più utilizzando il nostro potente sistema di temi. Per esempio, consulta i temi e componenti principali su meta.discourse.org.

    Divertiti e buona fortuna nel costruire la tua nuova comunità!

    search_logs: diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index fa71b8e791..6e27d7b5f5 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -1611,6 +1611,7 @@ ko: avatar_sizes: "자동 생성 아바타 사이즈 목록" external_system_avatars_enabled: "외부 아바타 시스템을 사용하기" external_system_avatars_url: "외부 아바타 서비스의 URL. {username} {first_letter} {color} {size} 의 대체가 허용됨" + use_site_small_logo_as_system_avatar: "시스템 사용자 아바타 대신 사이트의 작은 로고를 사용하십시오. 로고가 있어야 합니다." restrict_letter_avatar_colors: "문자 아바타 배경에 사용되는 6 자리 16 진수 색상 값 목록입니다." selectable_avatars_enabled: "사용자가 목록에서 아바타를 선택하도록합니다." selectable_avatars: "사용자가 선택할 수있는 아바타 목록." @@ -3356,6 +3357,7 @@ ko: 즐거운 시간 되세요! signup_after_reject: + title: "거부 후 가입" subject_template: "%{site_name}에서 거부되었습니다." signup: title: "가입하기" diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index a9201f07a4..ae54c12139 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -143,7 +143,7 @@ pl_PL: has_already_been_used: "został już użyty" inclusion: nie jest zawarty w liście invalid: jest nieprawidłowy - is_invalid: "wygląda niejasno, czy to cała wypowiedz ?" + is_invalid: "wygląda niejasno, czy to cała wypowiedź?" is_invalid_meaningful: "wydaje się niejasne, większość słów zawiera w kółko te same litery?" is_invalid_unpretentious: "wydaje się niejasne, jedno lub więcej słów jest bardzo długie?" is_invalid_quiet: "wydaje się niejasne, czy chciałeś wpisać to WIELKIMI LITERAMI?" @@ -370,7 +370,7 @@ pl_PL: slow_mode_enabled: "Ten temat jest w trybie zwolnionym." just_posted_that: "jest zbyt podobne do tego co umieściłeś niedawno" invalid_characters: "zawiera niepoprawne znaki" - is_invalid: "wygląda niejasno, czy to cała wypowiedz ?" + is_invalid: "wygląda niejasno, czy to cała wypowiedź?" next_page: "następna strona →" prev_page: "← poprzednia strona" page_num: "Strona %{num}" @@ -1530,7 +1530,7 @@ pl_PL: mobile_logo_dark: "Ciemna alternatywa dla ustawienia witryny 'mobile logo'." large_icon: "Obraz używany jako podstawa dla innych ikon metadanych. Idealnie powinna być większa niż 512 x 512. Jeśli pozostanie puste, zostanie użyte logo_small." manifest_icon: "Obraz używany jako logo / obraz powitalny na Androidzie. Zostanie automatycznie zmieniony na 512 × 512. Jeśli pozostanie puste, zostanie użyty duży_plik." - favicon: "Ulubiony dla Twojej witryny, zobacz https://en.wikipedia.org/wiki/Favicon . Aby działać poprawnie na CDN, musi to być png. Zostanie przeskalowany do 32x32. Jeśli pozostanie puste, zostanie użyty duży_plik." + favicon: "Favicona dla Twojej strony, zobacz https://en.wikipedia.org/wiki/Favicon. Aby działała poprawnie poprzez CDN, musi to być w formacie png. Zostanie przeskalowana do 32x32. Jeśli pozostanie pusta, zostanie użyte large_icon." apple_touch_icon: "Ikona używana dla urządzeń Apple Touch. Zostanie automatycznie zmieniony na 180x180. Jeśli pozostanie puste, zostanie użyty duży_plik." opengraph_image: "Domyślny obraz opengraph, używany, gdy strona nie ma innego odpowiedniego obrazu. Jeśli pozostanie puste, zostanie użyty duży_plik" twitter_summary_large_image: "„Podsumowanie dużego obrazu” karty Twittera (powinno mieć co najmniej 280 szerokości i co najmniej 150 wysokości). Jeśli pole pozostanie puste, generowane są zwykłe metadane karty przy użyciu opengraph_image." @@ -1831,14 +1831,14 @@ pl_PL: category_style: "Styl etykiet kategorii" default_dark_mode_color_scheme_id: "Schemat kolorów używany w trybie ciemnym." dark_mode_none: "Brak" - max_image_size_kb: "Maksymalny rozmiar przesyłanego obrazu w kB. Należy to również skonfigurować w nginx (rozmiar_maks_komputera_ klienta) / apache lub proxy. Obrazy większe niż to i mniejsze niż client_max_body_size zostaną przeskalowane, aby zmieściły się podczas przesyłania." + max_image_size_kb: "Maksymalny rozmiar przesyłanego obrazu w kB. Należy to również skonfigurować w nginx (client_max_body_size) / apache lub proxy. Obrazy większe niż ta wartość i mniejsze niż client_max_body_size zostaną przeskalowane, aby zmieściły się podczas przesyłania." max_attachment_size_kb: "Maksymalny rozmiar załącznika w kB. Należy to także skonfigurować dla serwera nginx (client_max_body_size) / Apache lub dla proxy." authorized_extensions: "Lista dozwolonych rozszerzeń plików (gwiazdka \"*\" oznacza wszystkie typy plików)" authorized_extensions_for_staff: "Lista rozszerzeń plików dozwolonych do przesłania dla użytkowników personelu oprócz listy zdefiniowanej w ustawieniach witryny „uprawnione_rozszerzenia”. (użyj „*”, aby włączyć wszystkie typy plików)" theme_authorized_extensions: "Lista dozwolonych rozszerzeń plików dla przesyłania szablonów (gwiazdka \"*\" oznacza wszystkie typy plików)" max_similar_results: "Liczba podobnych tematów, jaka zostanie wyświetlona nad oknem edytora podczas tworzenia nowego tematu. Tematy są porównywane na podstawie tytułów i treści." max_image_megapixels: "Maksymalna liczba megapikseli dozwolona dla obrazu. Obrazy o większej liczbie megapikseli zostaną odrzucone." - title_prettify: "Prevent common title typos and errors, including all caps, lowercase first character, multiple ! and ?, extra . at end, etc." + title_prettify: "Zapobiegaj typowym literówkom i błędom w tytułach, takimi jak cały tekst wielkimi literami, rozpoczęcie małą literą, wiele ! i ?, dodatkowa . na końcu, itp." title_remove_extraneous_space: "Usuń wiodące białe znaki przed interpunkcją końcową." automatic_topic_heat_values: 'Automatycznie aktualizuj ustawienia „widok tematów ciepło” i „wpis temat jak ciepło” na podstawie aktywności na stronie.' topic_views_heat_low: "Po tylu wyświetleniach liczba wyświetleń zostanie lekko wyróżniona." diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf index 6fd053d5d9..372efcc25a 100644 --- a/config/nginx.sample.conf +++ b/config/nginx.sample.conf @@ -29,7 +29,7 @@ map $http_x_forwarded_proto $thescheme { https https; } -log_format log_discourse '[$time_local] "$http_host" $remote_addr "$request" "$http_user_agent" "$sent_http_x_discourse_route" $status $bytes_sent "$http_referer" $upstream_response_time $request_time "$sent_http_x_discourse_username"'; +log_format log_discourse '[$time_local] "$http_host" $remote_addr "$request" "$http_user_agent" "$sent_http_x_discourse_route" $status $bytes_sent "$http_referer" $upstream_response_time $request_time "$upstream_http_x_discourse_username" "$upstream_http_x_discourse_trackview" "$upstream_http_x_queue_time" "$upstream_http_x_redis_calls" "$upstream_http_x_redis_time" "$upstream_http_x_sql_calls" "$upstream_http_x_sql_time"'; server { diff --git a/config/routes.rb b/config/routes.rb index e30b0a7d2c..85c4092044 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -260,6 +260,8 @@ Discourse::Application.routes.draw do get "dashboard/moderation" => "dashboard#moderation" get "dashboard/security" => "dashboard#security" get "dashboard/reports" => "dashboard#reports" + get "dashboard/new-features" => "dashboard#new_features" + put "dashboard/mark-new-features-as-seen" => "dashboard#mark_new_features_as_seen" resources :dashboard, only: [:index] do collection do diff --git a/config/site_settings.yml b/config/site_settings.yml index a64dba360c..6834aed5bb 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -541,7 +541,9 @@ users: max: 10 block_common_passwords: true username_change_period: 3 - email_editable: true + email_editable: + client: true + default: true logout_redirect: client: true default: "" @@ -2134,6 +2136,10 @@ uncategorized: client: true hidden: true + check_for_new_features: + default: false + hidden: true + automatically_unpin_topics: default: true client: true diff --git a/db/migrate/20210105165605_add_processed_to_notifications.rb b/db/migrate/20210105165605_add_processed_to_notifications.rb index 8c560f2de7..769d127546 100644 --- a/db/migrate/20210105165605_add_processed_to_notifications.rb +++ b/db/migrate/20210105165605_add_processed_to_notifications.rb @@ -1,14 +1,6 @@ # frozen_string_literal: true class AddProcessedToNotifications < ActiveRecord::Migration[6.0] - def up - add_column :notifications, :processed, :boolean, default: false - execute "UPDATE notifications SET processed = true" - change_column_null(:notifications, :processed, false) - add_index :notifications, [:processed], unique: false - end - - def down - remove_column :notifications, :processed + def change end end diff --git a/db/migrate/20210126222142_create_shelved_notifications.rb b/db/migrate/20210126222142_create_shelved_notifications.rb new file mode 100644 index 0000000000..2af5b2e86c --- /dev/null +++ b/db/migrate/20210126222142_create_shelved_notifications.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +class CreateShelvedNotifications < ActiveRecord::Migration[6.0] + def change + create_table :shelved_notifications do |t| + t.integer :notification_id, null: false + end + add_index :shelved_notifications, [:notification_id] + end +end diff --git a/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb b/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb new file mode 100644 index 0000000000..5d3f6e6d01 --- /dev/null +++ b/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class MigrateSearchDataAfterDefaultLocaleRename < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + + def up + %w{category tag topic user}.each { |model| fix_search_data(model) } + end + + def down + raise ActiveRecord::IrreversibleMigration + end + + private + + def fix_search_data(model) + key = "#{model}_id" + table = "#{model}_search_data" + + puts "Migrating #{table} to new locale." + + sql = <<~SQL + UPDATE #{table} + SET locale = 'en' + WHERE #{key} IN ( + SELECT #{key} + FROM #{table} + WHERE locale = 'en_US' + LIMIT 100000 + ) + SQL + + loop do + count = execute(sql).cmd_tuples + break if count == 0 + puts "Migrated #{count} rows of #{table} to new locale." + end + end +end diff --git a/db/post_migrate/20210127140730_undo_add_processed_to_notifications.rb b/db/post_migrate/20210127140730_undo_add_processed_to_notifications.rb new file mode 100644 index 0000000000..2904760628 --- /dev/null +++ b/db/post_migrate/20210127140730_undo_add_processed_to_notifications.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class UndoAddProcessedToNotifications < ActiveRecord::Migration[6.0] + def up + execute "ALTER TABLE notifications DROP COLUMN IF EXISTS processed" + end +end diff --git a/lefthook.yml b/lefthook.yml index 2e622162ee..50e4342d19 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -7,7 +7,7 @@ pre-commit: prettier: glob: "*.{js,es6}" include: "app/assets/javascripts|test/javascripts" - run: yarn prettier --list-different {staged_files} + run: yarn pprettier --list-different {staged_files} eslint-es6: glob: "*.es6" include: "app/assets/javascripts|test/javascripts" @@ -54,7 +54,7 @@ lints: prettier: glob: "*.{js,es6}" include: "app/assets/javascripts|test/javascripts" - run: yarn prettier --list-different {all_files} + run: yarn pprettier --list-different {all_files} eslint-assets-es6: run: yarn eslint --ext .es6 app/assets/javascripts eslint-assets-js: diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb index 6a7dc2a54b..a0274fe092 100644 --- a/lib/discourse_updates.rb +++ b/lib/discourse_updates.rb @@ -115,6 +115,42 @@ module DiscourseUpdates keys.present? ? keys.map { |k| Discourse.redis.hgetall(k) } : [] end + def perform_new_feature_check + response = Excon.new(new_features_endpoint).request(expects: [200], method: :Get) + json = JSON.parse(response.body) + Discourse.redis.set(new_features_key, response.body) + end + + def unseen_new_features(user_id) + entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil + return nil if entries.nil? + + last_seen = new_features_last_seen(user_id) + + if last_seen.present? + entries.select! { |item| Time.zone.parse(item["created_at"]) > last_seen } + end + + entries.select! do |item| + item["discourse_version"].nil? || Discourse.has_needed_version?(Discourse::VERSION::STRING, item["discourse_version"]) rescue nil + end + + entries.sort { |item| Time.zone.parse(item["created_at"]) } + end + + def new_features_last_seen(user_id) + last_seen = Discourse.redis.get new_features_last_seen_key(user_id) + return nil if last_seen.blank? + Time.zone.parse(last_seen) + end + + def mark_new_features_as_seen(user_id) + entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil + return nil if entries.nil? + last_seen = entries.max_by { |x| x["created_at"] } + Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"]) + end + private def last_installed_version_key @@ -144,5 +180,17 @@ module DiscourseUpdates def missing_versions_key_prefix 'missing_version' end + + def new_features_endpoint + 'https://meta.discourse.org/new-features.json' + end + + def new_features_key + 'new_features' + end + + def new_features_last_seen_key(user_id) + "new_features_last_seen_user_#{user_id}" + end end end diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb index 5fd20b2c75..d55d65edbf 100644 --- a/lib/plugin/metadata.rb +++ b/lib/plugin/metadata.rb @@ -82,7 +82,7 @@ class Plugin::Metadata "discourse-staff-alias", ]) - FIELDS ||= [:name, :about, :version, :authors, :url, :required_version, :transpile_js] + FIELDS ||= [:name, :about, :version, :authors, :contact_emails, :url, :required_version, :transpile_js] attr_accessor(*FIELDS) def self.parse(text) diff --git a/lib/timeline_lookup.rb b/lib/timeline_lookup.rb index 2e6cf57c82..f477153f94 100644 --- a/lib/timeline_lookup.rb +++ b/lib/timeline_lookup.rb @@ -12,7 +12,10 @@ module TimelineLookup last_days_ago = -1 tuples.each_with_index do |t, idx| return result unless t.is_a?(Array) - next unless (idx % every) === 0 + + if idx != tuples.size - 1 + next unless (idx % every) === 0 + end days_ago = t[1] diff --git a/lib/version.rb b/lib/version.rb index a5ea2d6ae2..d0b71da7c4 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -10,7 +10,7 @@ module Discourse MAJOR = 2 MINOR = 7 TINY = 0 - PRE = 'beta2' + PRE = 'beta3' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/package.json b/package.json index 784cb2e636..c2bb0e9788 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "bootstrap": "v3.4.1", "chart.js": "2.9.3", "chartjs-plugin-datalabels": "^0.7.0", - "eslint-config-discourse": "^1.1.5", + "eslint-config-discourse": "^1.1.8", "handlebars": "^4.7.0", "highlight.js": "https://github.com/highlightjs/highlight.js", "intersection-observer": "^0.5.1", @@ -38,6 +38,7 @@ "workbox-sw": "^4.3.1" }, "devDependencies": { + "@mixer/parallel-prettier": "^2.0.1", "@arkweid/lefthook": "^0.7.2", "chrome-launcher": "^0.12.0", "chrome-remote-interface": "^0.25", diff --git a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss index 937b8c8b7c..246ac3b4cb 100644 --- a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss +++ b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss @@ -201,7 +201,7 @@ flex: 1; @include ellipsis; width: 100%; - text-align: left; + justify-content: flex-start; } } diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 index 3cac3b63e0..156c4dd0cd 100644 --- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 +++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 @@ -13,26 +13,26 @@ function initialize(api) { }, }); - api.modifyClass("model:post", { - toggleBookmark() { + api.modifyClass("controller:topic", { + _togglePostBookmark(post) { // if we are talking to discobot then any bookmarks should just // be created without reminder options, to streamline the new user // narrative. const discobotUserId = -2; - if (this.user_id === discobotUserId && !this.bookmarked) { + if (post.user_id === discobotUserId && !post.bookmarked) { return ajax("/bookmarks", { type: "POST", - data: { post_id: this.id }, + data: { post_id: post.id }, }).then((response) => { - this.setProperties({ + post.setProperties({ "topic.bookmarked": true, bookmarked: true, bookmark_id: response.id, }); - this.appEvents.trigger("post-stream:refresh", { id: this.id }); + post.appEvents.trigger("post-stream:refresh", { id: this.id }); }); } - return this._super(); + return this._super(post); }, }); diff --git a/plugins/discourse-narrative-bot/config/locales/server.ar.yml b/plugins/discourse-narrative-bot/config/locales/server.ar.yml index 49e8089cb3..42292c5977 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ar.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ar.yml @@ -24,7 +24,6 @@ ar: long_description: | تُمنح هذه الشارة عند الإنتهاء بنجاح من الدرس التعليمي التفاعلي المتقدم للأعضاء, انت الأن تتقن الأدوات المُتقدمة في الموقع - و انت الان تحمل رخصة مُتقدمة! discourse_narrative_bot: - bio: "مرحباً, انا لستُ شخص حقيقي, انا الروبوت الذي سيقوم بتوجيهك لكي تتعلم استخدام ادوات هذا الموقع, لكي تتفاعل معي ارسل لي رسالة او اشر الي بـ**`@%{discobot_username}`** في اي مكان." timeout: message: |- مرحباً @%{username}, انا فقط اتفقدك لانك غائب منذ وقت طويل. diff --git a/plugins/discourse-narrative-bot/config/locales/server.be.yml b/plugins/discourse-narrative-bot/config/locales/server.be.yml index 19b85cd56a..f0f9eceeed 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.be.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.be.yml @@ -24,7 +24,6 @@ be: long_description: | Гэты знак выдаецца пасля паспяховага завяршэння інтэрактыўнага прасунутага карыстальніка падручніка. Вы асвоілі перадавыя інструменты абмеркавання - і зараз вы цалкам ліцэнзаваны! discourse_narrative_bot: - bio: "Прывітанне, я не сапраўдны чалавек. Я бот, які можа навучыць вас аб гэтым сайце. Для таго, каб ўзаемадзейнічаць са мной, адпраўце мне паведамленне або любое ** `@% {discobot_username}` ** ў любым месцы." timeout: message: |- Эй @% {імя карыстальніка}, проста праверыць, таму што я не чуў ад вас у нейкі час.- Для таго, каб працягнуць, адказаць мне ў любы час.- Калі вы хочаце, каб прапусціць гэты крок, скажам, `% {skip_trigger}`.- Для таго, каб пачаць усё спачатку, скажам, `% {reset_trigger}`.Калі вы не хочаце, гэта таксама нармальна. Я робат. Вы не пашкодзіце мае пачуцці. : Рыданні: diff --git a/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml b/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml index a6bbdaffbe..f857ea6c4b 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml @@ -25,7 +25,6 @@ bs_BA: long_description: | Ovaj bedž je dodjeljen prilikom uspješnog završetka interaktivnog tutorijala za nove korisnike. Postali ste majstor naprednih alata u diskusiji - sada ste potpuno licencirani! discourse_narrative_bot: - bio: "Pozdrav, ja nisam stvarna osoba. Ja sam automatski robot koji će vas naučiti kako da koristite ovaj web sajt. Kako bi komunicirali sa mnom, pošaljite mi poruku ili me spomenite pisajući **`@%{discobot_username}`** bilo gdje." timeout: message: |- Hej @%{username}, samo vas provjeravam jer nisam vas čuo neko vrijeme. diff --git a/plugins/discourse-narrative-bot/config/locales/server.ca.yml b/plugins/discourse-narrative-bot/config/locales/server.ca.yml index 4a0525bec5..15ac541986 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ca.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ca.yml @@ -24,7 +24,6 @@ ca: long_description: | Aquesta insígnia es concedeix després d'haver completat amb èxit el tutorial interactiu d'usuari avançat. Heu dominat les eines avançades de discussió, i ara teniu esteu completament acreditat! discourse_narrative_bot: - bio: "Hola, no sóc una persona real, sóc un robot que us pot ensenyar coses sobre aquest lloc web. Per a interactuar amb mi, envieu-me un missatge o feu una menció a **%{discobot_username}** en qualsevol lloc. " timeout: message: |- Bon dia @%{username}, diff --git a/plugins/discourse-narrative-bot/config/locales/server.cs.yml b/plugins/discourse-narrative-bot/config/locales/server.cs.yml index f71d0f074c..02c3718cd1 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.cs.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.cs.yml @@ -23,7 +23,6 @@ cs: long_description: | Tento odznak je udělen za úspěšné dokončení pokročilého interaktivního tutoriálu pro uživatele. Zvládli jste pokročilé nástroje diskuse — a nyní jste plně licencováni! discourse_narrative_bot: - bio: "Ahoj, já nejsem člověk. Jsem robot, který Vás může naučit něco o tomto webu. Pro interakci se mnou mi pošlete zprávu nebo kdekoliv zmiňte **%{discobot_username}**." timeout: message: |- Ahoj @%{username}, jen se ozývám, protože jsem od Vás nějakou dobu nic neslyšel. diff --git a/plugins/discourse-narrative-bot/config/locales/server.da.yml b/plugins/discourse-narrative-bot/config/locales/server.da.yml index ba41ba3169..a8e1006eb7 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.da.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.da.yml @@ -24,7 +24,6 @@ da: long_description: | Denne badge tildeles, når den interaktive avancerede brugervejledning er afsluttet. Du har mestret de avancerede diskussionsværktøjer - og nu er du fuldt licenseret! discourse_narrative_bot: - bio: "Hej, jeg er ikke en rigtigt person. Jeg er en bot der kan lære dig om dette site. For at interaggere med mig, send mig en besked, eller nævn **`@%{discobot_username}`** overalt." timeout: message: |- Hey @%{username}, jeg skriver fordi jeg ikke har hørt fra dig i et stykke tid. diff --git a/plugins/discourse-narrative-bot/config/locales/server.de.yml b/plugins/discourse-narrative-bot/config/locales/server.de.yml index 36b9ad8e7f..84d4727e56 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.de.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.de.yml @@ -25,7 +25,7 @@ de: long_description: | Das Abzeichen wird verliehen, wenn das interaktive Tutorial für fortgeschrittene Benutzer erfolgreich abgeschlossen wurde. Du beherrscht die fortgeschrittenen Werkzeuge für Diskussionen erlernt und besitzt nun die Lizenz zum Diskutieren. discourse_narrative_bot: - bio: "Hallo! Ich bin keine reale Person. Ich bin ein Bot, der dir etwas über diese Website beibringen kann. Schick mir eine Nachricht oder erwähne irgendwo **`@%{discobot_username}`**, um mit mir zu interagieren." + bio: "Hallo! Ich bin keine reale Person. Ich bin ein Bot, der dir etwas über diese Website beibringen kann. Schick mir eine Nachricht oder erwähne mich namentlich, um mit mir zu interagieren." tl2_promotion_message: subject_template: "Jetzt, da Sie befördert wurden, ist es Zeit, etwas über einige erweiterte Funktionen zu erfahren!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.el.yml b/plugins/discourse-narrative-bot/config/locales/server.el.yml index 308f5d7ae0..875392862e 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.el.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.el.yml @@ -25,7 +25,6 @@ el: long_description: | Αυτό το σήμα χορηγείται μετά την επιτυχή ολοκλήρωση του διαδραστικού φροντιστηρίου προχωρημένου χρήστη. Κατακτήσατε τα προηγμένα εργαλεία συζήτησης - και τώρα έχετε πλήρη άδεια! discourse_narrative_bot: - bio: "Γεια σας, δεν είμαι πραγματικό πρόσωπο - είμαι ένα bot που μπορεί να σας διδάξει για αυτήν την ιστοσελίδα. Για να επικοινωνήσετε μαζί μου, στείλτε μου απλά ένα μήνυμα ή αναφέρετε το **`@%{discobot_username}`** οπουδήποτε." timeout: message: |- Γεια σας @%{username}, δεν είχα νέα σας εδώ και λίγο καιρό και ειπα να δω τι κάνετε. diff --git a/plugins/discourse-narrative-bot/config/locales/server.en.yml b/plugins/discourse-narrative-bot/config/locales/server.en.yml index 750201811e..2d4887f4be 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.en.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.en.yml @@ -21,7 +21,7 @@ en: This badge is granted upon successful completion of the interactive advanced user tutorial. You’ve mastered the advanced tools of discussion — and now you’re fully licensed! discourse_narrative_bot: - bio: "Hi, I’m not a real person. I’m a bot that can teach you about this site. To interact with me, send me a message or mention **`@%{discobot_username}`** anywhere." + bio: "Hi, I’m not a real person. I’m a bot that can teach you about this site. To interact with me, send me a message or mention me by name." tl2_promotion_message: subject_template: "Now that you’ve been promoted, it’s time to learn about some advanced features!" diff --git a/plugins/discourse-narrative-bot/config/locales/server.en_GB.yml b/plugins/discourse-narrative-bot/config/locales/server.en_GB.yml index 10362c37cd..716fbbc72d 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.en_GB.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.en_GB.yml @@ -5,6 +5,10 @@ # https://translate.discourse.org/ en_GB: + badges: + certified: + long_description: | + This badge is granted upon successful completion of the interactive new user tutorial. You’ve taken the initiative to learn the basic tools of discussion, and now you have been certified! discourse_narrative_bot: advanced_user_narrative: start_message: |- diff --git a/plugins/discourse-narrative-bot/config/locales/server.es.yml b/plugins/discourse-narrative-bot/config/locales/server.es.yml index 03bd0a095b..3ba17114db 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.es.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.es.yml @@ -25,7 +25,6 @@ es: long_description: | Esta medalla se otorga al completar con éxito el tutorial interactivo para usuarios avanzados. Has dominado las herramientas avanzadas de discusión y ¡ahora tienes licencia completa! discourse_narrative_bot: - bio: "¡Hola! No soy una persona real, soy un bot que te puede enseñar acerca este sitio. Para interactuar conmigo, envíame un mensaje o menciona **`%{discobot_username}`** en cualquier lugar." timeout: message: |- Hey, @%{username}, te quería decir que estoy pendiente de ti porque no he tenido noticias tuyas en mucho tiempo. diff --git a/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml b/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml index a2df697968..df87fc1ea1 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml @@ -23,7 +23,6 @@ fa_IR: long_description: | این مدال وقتی که راهنمای پیشرفته‌ کاربران با موفقیت به پایان رسیده باشد، اعطا می‌گردد. شما ابزارهای پیشرفته‌ی گفتگو را فرا گرفته اید و الان دارای مجوز کاملید! discourse_narrative_bot: - bio: "سلام. من یک فرد واقعی نیستم. من رباتی هستم که می‌توانم در رابطه با این تارنما به شما آموزش بدهم. برای اینکه با من تعامل داشته باشی، برایم پیام بفرست یا با استفاده از **`@%{discobot_username}`** به من اشاره بکن." timeout: message: |- سلام @%{username}، خواستم احوالت را جویا بشم چون مدتی بود از تو نشنیدم. diff --git a/plugins/discourse-narrative-bot/config/locales/server.fi.yml b/plugins/discourse-narrative-bot/config/locales/server.fi.yml index 09f332b8a7..ac00ae3f0f 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.fi.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.fi.yml @@ -24,7 +24,6 @@ fi: long_description: | Tämä ansiomerkki myönnetään, kun suoritat menestyksellä vuorovaikutteisen palstan käytön jatkokurssin. Olet omaksunut edistyneemmätkin toiminnot, ja olet taitosi osoittanut. discourse_narrative_bot: - bio: "Moi! En ole ihminen. Olen botti, ja tarjoan opetusta sivuston käyttämisestä. Jos haluat jutella, lähetä yksityisviesti tai mainitse **`@%{discobot_username}`** missä vain." timeout: message: |- Hei @%{username}, kyselen kuulumisia, kun en ole kuullut sinusta hetkeen. diff --git a/plugins/discourse-narrative-bot/config/locales/server.fr.yml b/plugins/discourse-narrative-bot/config/locales/server.fr.yml index 836c8d8df5..7791ed4d7d 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.fr.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.fr.yml @@ -25,7 +25,6 @@ fr: long_description: | Ce badge est décerné quand vous avez terminé avec succès le tutoriel interactif des utilisateurs avancés. Vous avez pris l'initiative d'apprendre les outils avancés de la discussion et vous êtes maintenant certifié à 100% ! discourse_narrative_bot: - bio: "Bonjour, je ne suis pas une personne réelle. Je suis un robot pour vous faire découvrir ce site. Pour interagir avec moi, envoyez-moi un message ou mentionnez **`@%{discobot_username}`** n'importe où." tl2_promotion_message: subject_template: "Maintenant que vous avez été promu, il est temps d'en apprendre davantage sur certaines fonctionnalités avancées !" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.gl.yml b/plugins/discourse-narrative-bot/config/locales/server.gl.yml index 8105991e5e..4ffa2f898c 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.gl.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.gl.yml @@ -25,7 +25,6 @@ gl: long_description: | Esta insignia conséguese tras completar correctamente o titorial interactivo avanzado de novo usuario. Adquiriu as ferramentas avanzadas de discusión — e agora está totalmente licenciado! discourse_narrative_bot: - bio: "Ola, eu non son unha persoa real. Son un bot que pode instruílo sobre este sitio. Para interactuar comigo, envíeme unha mensaxe ou mención **`@%{discobot_username}`** en calquera sitio." tl2_promotion_message: subject_template: "Agora que foi promocionado, é o momento de aprender sobre algunhas funcionalidades avanzadas!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.he.yml b/plugins/discourse-narrative-bot/config/locales/server.he.yml index 99ed8ea099..1a68d9d536 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.he.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.he.yml @@ -25,7 +25,7 @@ he: long_description: | עיטור זה מוענק עם השלמת המדריך האינטראקטיבי משתמשים מתקדמים אינטראקטיבי בהצלחה. צברת מומחיות בתחום כלי הדיון המתקדמים וכעת קיבלת רישיון! discourse_narrative_bot: - bio: "היי, אני לא בן אדם אמיתי. אני בוט שיכול ללמד אותך על האתר הזה. כדי לתקשר איתי, יש לשלוח אלי הודעה או לאזכר את **‎`@%{discobot_username}`** בכל מקום שהוא." + bio: "היי, אני לא בן אדם אמיתי. אני בוט שיכול ללמד אותך על האתר הזה. כדי לתקשר איתי, אפשר לשלוח אלי הודעה או לאזכר אותי בשמי." tl2_promotion_message: subject_template: "עכשיו מכשקודמת, הגיע הזמן ללמוד על כמה מהתכונות המתקדמות!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.hu.yml b/plugins/discourse-narrative-bot/config/locales/server.hu.yml index a7efe4a8b6..274319c485 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.hu.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.hu.yml @@ -24,7 +24,6 @@ hu: long_description: | Ez a jelvényt az interaktív haladó felhasználói eligazítás sikeres teljesítéséért jár. Elsajátította a beszélgetés speciális eszközeit, és ezzel teljesen hivatalos lett! discourse_narrative_bot: - bio: "Üdv, nem igazi személy vagyok. Egy bot vagyok, és megtanítom az oldal használatát. A velem történő interakcióhoz küldjön nekem egy üzenetet, vagy említsen meg így bárhol: **`@%{discobot_username}`**." timeout: message: |- Üdv @%{username}, csak beköszönök, mert nem hallottam Önről egy ideje. diff --git a/plugins/discourse-narrative-bot/config/locales/server.hy.yml b/plugins/discourse-narrative-bot/config/locales/server.hy.yml index 416e47cf33..45db592035 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.hy.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.hy.yml @@ -24,7 +24,6 @@ hy: long_description: | Այս կրծքանշանը շնորհվում է օգտատիրոջ ընդլայնված փոխգործուն ձեռնարկի հաջող ավարտի կապակցությամբ: Դուք տիրապետում եք քննարկման ընդլայնված գործիքներին — և այժմ ամբողջովին լիցենզավորված եք! discourse_narrative_bot: - bio: "Բարև, ես իրական մարդ չեմ: Ես ռոբոտ եմ, որ կարող է սովորեցնել Ձեզ այս կայքի մասին: Ինձ հետ շփվելու համար ուղարկեք ինձ հաղորդագրություն կամ հիշատակեք **`@%{discobot_username}`** ցանկացած տեղ:" timeout: message: |- Հեյ @%{username}, ուղղակի ստուգում եմ, քանի որ արդեն որոշ ժամանակ է՝ քեզնից լուր չկա: diff --git a/plugins/discourse-narrative-bot/config/locales/server.id.yml b/plugins/discourse-narrative-bot/config/locales/server.id.yml index 7cfc9df992..786f6449e6 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.id.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.id.yml @@ -24,7 +24,6 @@ id: long_description: | Lencana ini diberikan atas keberhasilan menyelesaikan panduan interaktif pengguna anggota forum lanjutan. Anda telah ahli menggunakan fasilitas lanjutan dalam diskusi — sekarang Anda telah bersertifikat penuh menjadi anggota forum. discourse_narrative_bot: - bio: "Salam, saya bukan manusia asli. Saya adalah robot yang akan memandu Anda menggunakan forum ini. Untuk berkomunikasi dengan saya, kirimi saya pesan atau panggil **`@%{discobot_username}`** dimana saja." timeout: message: |- Salam @%{username}, hanya ingin menyapa karena saya tidak melihat Anda beberapa waktu. diff --git a/plugins/discourse-narrative-bot/config/locales/server.it.yml b/plugins/discourse-narrative-bot/config/locales/server.it.yml index 1e06ed0001..09838c20de 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.it.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.it.yml @@ -25,7 +25,6 @@ it: long_description: | Questo distintivo è assegnato al completamento del tutorial interattivo per utenti avanzati. Hai imparato gli strumenti avanzati di discussione — e ora sei un diplomato! discourse_narrative_bot: - bio: "Ciao, io non sono una persona reale. Sono un robot che ti può insegnare ad usare questo sito. Per interagire con me inviami un messaggio o menziona**`@%{discobot_username}`** ovunque." tl2_promotion_message: subject_template: "Ora che sei stato promosso, è il momento di conoscere alcune funzionalità avanzate!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.ja.yml b/plugins/discourse-narrative-bot/config/locales/server.ja.yml index 56f8e8f347..fa2edf3bd2 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ja.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ja.yml @@ -23,7 +23,6 @@ ja: long_description: | このバッジは上級ユーザーチュートリアルの優れた結果において授与されました。あなたは上級ディスカッションツールをマスターし、完全に許可されました。 discourse_narrative_bot: - bio: "初めまして。私は実在する人ではなく、このサイトについて教えることができるコンピュータプログラムです。私と情報交換するためにはメッセージを送信するか%{discobot_username}のどこかでちょっと話を出しておいて下さい。" timeout: message: |- こんにちは%{username}、しばらく便りがなかったのでチェックをしているところです。 diff --git a/plugins/discourse-narrative-bot/config/locales/server.ko.yml b/plugins/discourse-narrative-bot/config/locales/server.ko.yml index de01b67546..79465e0dea 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ko.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ko.yml @@ -25,7 +25,7 @@ ko: long_description: | 이 배지는 고급 유저 튜토리얼을 성공적으로 완료한 사용자에게 부여됩니다. 당신은 고급 토론 도구를 숙지하여 면허를 취득하였습니다! discourse_narrative_bot: - bio: "안녕, 사실 나는 사람이 아니야. 이 사이트에 대해서 알려주는 로봇이지. 나한테 물어보고 싶은 게 있다면 메세지를 보내거나 **`@%{discobot_username}`** 를 멘션하면 돼." + bio: "안녕하세요, 저는 진짜 사람이 아닙니다. 나는 이 사이트에 대해 알려주는 봇입니다. 저와 소통하려면 저에게 메시지를 보내거나 이름으로 저를 멘션하세요." tl2_promotion_message: subject_template: "이제 승격 했으므로 몇 가지 고급 기능에 대해 알아볼 차례입니다!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml b/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml index 209caa37e4..cc112fffef 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml @@ -23,7 +23,6 @@ nb_NO: long_description: | Dette merket er tildelt ved fullførelse av den interaktive opplæringen for avanserte brukere. Du har mestret de avanserte verktøyene for diskusjoner — og er nå fullstendig lisensiert! discourse_narrative_bot: - bio: "Hei, jeg er ikke en virkelig person. Jeg er en robot som kan lære deg om dette nettstedet. For å kommunisere med meg send meg en melding eller nevn **`@%{discobot_username}`** hvor som helst." timeout: message: |- Hei @%{username}, jeg bare sjekker opp på deg fordi jeg har ikke hørt fra deg på en stund. diff --git a/plugins/discourse-narrative-bot/config/locales/server.nl.yml b/plugins/discourse-narrative-bot/config/locales/server.nl.yml index 6b7c14e1dc..7d591f10e9 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.nl.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.nl.yml @@ -25,7 +25,6 @@ nl: long_description: | Deze badge wordt toegekend wanneer de interactieve handleiding voor gevorderde gebruikers met succes is doorlopen. U hebt de geavanceerde hulpmiddelen voor discussie onder de knie – en u bent nu volledig gecertificeerd! discourse_narrative_bot: - bio: "Hallo, ik ben geen echt persoon. Ik ben een bot die u uitleg over deze website kan geven. Stuur mij een bericht of noem ergens **`@%{discobot_username}`** om met mij te communiceren." dice: trigger: "gooien" invalid: |- diff --git a/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml b/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml index 3f0c228439..4b50e448ce 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml @@ -25,7 +25,7 @@ pl_PL: long_description: | Odznaka została nadana za ukończenie interaktywnego tutorialu dla zaawansowanych użytkowników. Poznałeś zaawansowane narzędzia dyskusji i jesteś teraz w pełni licencjonowany! discourse_narrative_bot: - bio: "Witaj, nie jestem prawdziwą osobą. Jestem botem, który może nauczyć Cię korzystania z tej witryny. Aby skomunikować się ze mną, wyślij do mnie wiadomość lub oznacz **`@%{discobot_username}`** w dowolnym miejscu." + bio: "Witaj, nie jestem prawdziwą osobą. Jestem botem, który może nauczyć Cię korzystania z tej witryny. Aby wejść ze mną w interakcję, wyślij do mnie wiadomość lub wspomnij w dowolnym miejscu." tl2_promotion_message: subject_template: "Teraz, gdy zostałeś awansowany, czas poznać kilka zaawansowanych funkcji!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.pt.yml b/plugins/discourse-narrative-bot/config/locales/server.pt.yml index 61e6a4ea74..9bfcbfc9cc 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.pt.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.pt.yml @@ -22,7 +22,6 @@ pt: name: Licenciado description: "Completou o nosso tutorial de utilizador avançado" discourse_narrative_bot: - bio: "Olá. Não sou humano. Sou um bot que pode ensiná-lo acerca deste sítio. Para interagir comigo, envie-me uma mensagem ou mencione-me %{discobot_username} em qualquer lado." dice: results: |- > :game_die: %{results} diff --git a/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml b/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml index 7bd2999231..da9e06b41b 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml @@ -24,7 +24,6 @@ pt_BR: long_description: | Este emblema é concedido após a conclusão bem-sucedida do tutorial interativo de usuário avançado. Você dominou as ferramentas avançadas de discussão — e agora está totalmente licenciado! discourse_narrative_bot: - bio: "Oi, eu não sou uma pessoa real. Eu sou um robô que pode te ensinar sobre este site. Para interagir comigo, me envie uma mensagem ou mencione **`@%{discobot_username}`** em qualquer lugar." tl2_promotion_message: subject_template: "Agora que você foi promovido, é hora de aprender sobre alguns recursos avançados!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.ru.yml b/plugins/discourse-narrative-bot/config/locales/server.ru.yml index 8ffe45fe69..74bc0fcbf1 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ru.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ru.yml @@ -25,7 +25,7 @@ ru: long_description: | Эта награда выдается при успешном завершении дополнительного обучения пользователя. Вы освоили все инструменты форума, теперь вы полностью готовы к общению! discourse_narrative_bot: - bio: "Здравствуйте! Я не реальный человек. Я бот, который может научить вас работать с этим сайтом. Чтобы работать со мной, отправьте мне сообщение или упоминание **`@%{discobot_username}`** в любом сообщении." + bio: "Привет, я не человек. Я бот, который может рассказать вам много интересного об этом сайте. Чтобы пообщаться со мной, отправьте мне личное сообщение или упомяните меня по имени в любом другом сообщении." tl2_promotion_message: subject_template: "Теперь, когда вы были повышены, пришло время узнать о дополнительном функционале форума!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.sk.yml b/plugins/discourse-narrative-bot/config/locales/server.sk.yml index 3679f372f4..cd5bc06d4e 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sk.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sk.yml @@ -23,7 +23,6 @@ sk: long_description: | Tento odznak sa udeľuje po úspešnom ukončení interaktívneho manuálu pokročilého používateľa. Zvládli ste pokročilé nástroje diskusie — a teraz ste plne certifikovaný! discourse_narrative_bot: - bio: "Ahoji, ja nie som skutočná osoba. Som robot, ktorý Ťa môže naučiť o tejto stránke. Komunikovať so mnou môžeš poslaním správy, alebo zmienkou **`@%{discobot_username}`** kdekoľvek." timeout: message: |- Hej @%{username}, iba kontrolujem, pretože nejaký čas som od Ťeba nič nepočul . diff --git a/plugins/discourse-narrative-bot/config/locales/server.sl.yml b/plugins/discourse-narrative-bot/config/locales/server.sl.yml index 8e6020781e..2fe44d3a38 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sl.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sl.yml @@ -25,7 +25,6 @@ sl: long_description: | Ta značka vam je podeljena po uspešno opravljenem vodiču za napredne uporabnike. Sedaj obvladate napredna orodja za razpravo - in tako ste licencirani! discourse_narrative_bot: - bio: "Pozdravljeni, jaz nisem prava oseba. Sem robot, ki te lahko naučim o tej strani. Za pogovor z menoj mi pošlji sporočilo ali me omeni z **`@%{discobot_username}`**." timeout: message: |- Pozdravljeni @%{username}, samo preverjam, ker vas že nekaj časa nisem slišal. diff --git a/plugins/discourse-narrative-bot/config/locales/server.sv.yml b/plugins/discourse-narrative-bot/config/locales/server.sv.yml index 18ff47cade..4b48dafdc4 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sv.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sv.yml @@ -25,7 +25,7 @@ sv: long_description: | Denna utmärkelse tilldelas efter att den interaktiva vägledningen för avancerade användare har genomförts. Du har lärt dig om de avancerade diskussionsverktygen och är nu certifierad! discourse_narrative_bot: - bio: "Hej, Jag är inte en riktig person. Jag är en bot som kan lära dig använda denna site. För att interagera med mig skickar du mig ett meddelande eller nämner **`@%{discobot_username}`** någonstans." + bio: "Hej, jag är ingen riktig person. Jag är en bot som kan lära dig mer om den här webbplatsen. För att interagera med mig, skicka ett meddelande till mig eller nämna mig med namn." tl2_promotion_message: subject_template: "Nu när du har blivit befordrad är det dags att lära dig mer om några avancerade funktioner!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.sw.yml b/plugins/discourse-narrative-bot/config/locales/server.sw.yml index 6a09f89bd9..fa5c8fb8eb 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sw.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sw.yml @@ -24,7 +24,6 @@ sw: long_description: | Hii beji inapewa kwa mtu aliyemaliza kupitia mafunzo yetu ya juu. Umepata mafunzo hayo ya juu kuhusu majadiliano - na sasa umethibitishwa! discourse_narrative_bot: - bio: "Habari, mimi sio mtu bali ni roboti. Ningependa nikufundishe kuhusu hii tovuti. Kuwasiliana na mimi, nitumie ujumbe au nitaje **@%{discobot_username}** popote." timeout: message: |- Habari @%{username}, nakujulia hali kwa sababu sijakuona nina mda diff --git a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml index 13c55ac985..910de7db09 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml @@ -24,7 +24,6 @@ tr_TR: long_description: | Bu rozet, interaktif ileri kullanıcı eğitiminin başarılı tamamlanmasının ardından verilir. Gelişmiş tartışma araçlarına hakim oldunuz - ve şimdi tamamen lisanslısınız! discourse_narrative_bot: - bio: "Merhaba, ben gerçek bir insan değilim. Bu site hakkındaki şeyleri size öğretebilecek bir robotum. Benimle etkileşime geçmek için bana bir mesaj gönderin veya ** `@%{discobot_username}` ** şekliyle herhangi bir yerde beni etiketleyin." timeout: message: |- Merhaba @ %{username}, uzun süredir sizden haber alamadık. diff --git a/plugins/discourse-narrative-bot/config/locales/server.uk.yml b/plugins/discourse-narrative-bot/config/locales/server.uk.yml index 4be90a71e7..6cea796606 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.uk.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.uk.yml @@ -25,7 +25,6 @@ uk: long_description: | Цей знак надається після успішного завершення інтерактивного навчального посібника для просунутих користувачів. Ви освоїли передові інструменти обговорення – і тепер ви повністю ліцензовані! discourse_narrative_bot: - bio: "Привіт, я не справжня людина. Я бот, який може навчити вас роботі з цим сайтом. Щоб спілкуватися зі мною, надішліть мені повідомлення або згадайте ** `@ %{discobot_username}` ** де завгодно." tl2_promotion_message: subject_template: "Тепер, коли ви були підвищені, настав час дізнатися про деякі додаткові функції!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.ur.yml b/plugins/discourse-narrative-bot/config/locales/server.ur.yml index c31dc6fba4..11357fd54a 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ur.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ur.yml @@ -24,7 +24,6 @@ ur: long_description: | یہ بَیج انٹرایکٹِو اَیڈوانسڈ ٹیوٹوریل کی کامیاب تکمیل پر دیا جاتا ہے۔ آپ نے بحث کے اعلیٰ اصولوں پر مہارت حاصل کر لی ہے — اور اب آپ مکمل طور پر لائسنس یافتہ ہیں! discourse_narrative_bot: - bio: "ہیلو، میں ایک حقیقی شخص نہیں ہوں۔ میں ایک بَوٹ ہوں جو آپ کو اس سائٹ کے بارے میں سکھا سکتا ہے۔ میرے ساتھ بات چیت کرنے کے لئے، مجھے ایک پیغام بھیجیں یا کہیں بھی **`@%{discobot_username}`** زکر کریں۔" timeout: message: |- ارے @%{username}، بس چیک کر رہا ہوں کیونکہ میں نے کچھ دیر سے آپ سے نہیں سنا۔ diff --git a/plugins/discourse-narrative-bot/config/locales/server.vi.yml b/plugins/discourse-narrative-bot/config/locales/server.vi.yml index 7fd7646588..49426665d8 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.vi.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.vi.yml @@ -19,7 +19,6 @@ vi: name: Đã cấp phép description: "Đã hoàn thành hướng dẫn nâng cao cho thành viên mới của chúng tôi" discourse_narrative_bot: - bio: "Xin chào, tôi không phải là người thật. Tôi là một bot có thể dạy bạn về trang web này. Để tương tác với tôi, hãy gửi cho tôi một tin nhắn hoặc đề cập đến ** `@%{discobot_username}'** ở bất kỳ đâu." quote: "3": quote: "Đừng khóc vì nó đã qua, hãy cười vì điều đó đã xảy ra." diff --git a/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml b/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml index 578b6f5e1e..ae41375ec9 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml @@ -25,7 +25,7 @@ zh_CN: long_description: | 该徽章授予给完成交互式高级用户教程的你。你已经掌握了讨论的高级工具——本徽章就是你的证明! discourse_narrative_bot: - bio: "你好,我不是真人,而是一个教你如何使用站点的机器人。你可以给我发消息或在任意地方提及 **`@%{discobot_username}`** 与我交互。" + bio: "你好,我不是一个真实存在的人类。我是一个可以带你了解这个站点的机器人。你可以通过给我发消息或提及我来与我交互。" tl2_promotion_message: subject_template: "现在你已经晋升了,是时候学习高级功能了!" text_body_template: | diff --git a/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb b/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb index 527dfbe3c9..9586bc483f 100644 --- a/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb +++ b/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb @@ -49,7 +49,7 @@ bot.create_user_profile! if !bot.user_profile if !bot.user_profile.bio_raw bot.user_profile.update!( - bio_raw: I18n.t('discourse_narrative_bot.bio', site_title: SiteSetting.title, discobot_username: bot.username) + bio_raw: I18n.t('discourse_narrative_bot.bio') ) end diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb index e5bccc0823..040bb7d9b0 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb @@ -46,7 +46,7 @@ <%= @name %> - <%= @logo_group %> + <%== @logo_group %> diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb index 298f6119c7..d89108ae8e 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb @@ -457,7 +457,7 @@ <%= @name %> - <%= @logo_group %> + <%== @logo_group %> diff --git a/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb b/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb index d30c76cf6a..54a7f5399c 100644 --- a/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb +++ b/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb @@ -36,6 +36,7 @@ describe "Discobot Certificate" do get '/discobot/certificate.svg', params: params expect(response.status).to eq(200) + expect(response.body).to include('') end describe 'when params are missing' do diff --git a/plugins/discourse-presence/config/locales/client.hu.yml b/plugins/discourse-presence/config/locales/client.hu.yml index d57a4b99da..676d10b79a 100644 --- a/plugins/discourse-presence/config/locales/client.hu.yml +++ b/plugins/discourse-presence/config/locales/client.hu.yml @@ -7,6 +7,12 @@ hu: js: presence: + replying: + one: "válaszol" + other: "válaszol" + editing: + one: "szerkeszt" + other: "szerkeszt" replying_to_topic: one: "válaszol" other: "válaszol" diff --git a/plugins/lazy-yt/assets/javascripts/lazyYT.js b/plugins/lazy-yt/assets/javascripts/lazyYT.js index d131a7924a..8078671868 100644 --- a/plugins/lazy-yt/assets/javascripts/lazyYT.js +++ b/plugins/lazy-yt/assets/javascripts/lazyYT.js @@ -15,7 +15,7 @@ "use strict"; function setUp($el, settings) { - var width = $el.data("width"), + let width = $el.data("width"), height = $el.data("height"), ratio = $el.data("ratio") ? $el.data("ratio") : settings.default_ratio, id = $el.data("youtube-id"), @@ -88,7 +88,7 @@ innerHtml.push("
    "); // .html5-title-text-wrapper innerHtml.push("
    "); // end of Video title .html5-info-bar - var prefetchedThumbnail = $el[0].querySelector(".ytp-thumbnail-image"); + let prefetchedThumbnail = $el[0].querySelector(".ytp-thumbnail-image"); $el .css({ @@ -158,15 +158,15 @@ } $.fn.lazyYT = function (newSettings) { - var defaultSettings = { + let defaultSettings = { default_ratio: "16:9", callback: null, // ToDO execute callback if given container_class: "lazyYT-container", }; - var settings = $.extend(defaultSettings, newSettings); + let settings = $.extend(defaultSettings, newSettings); return this.each(function () { - var $el = $(this).addClass(settings.container_class); + let $el = $(this).addClass(settings.container_class); setUp($el, settings); }); }; diff --git a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 index 9f0d033b79..e004aefa75 100644 --- a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 +++ b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 @@ -302,7 +302,7 @@ export function setup(helper) { * http://www.opensource.org/licenses/bsd-license */ function md5cycle(x, k) { - var a = x[0], + let a = x[0], b = x[1], c = x[2], d = x[3]; @@ -406,14 +406,14 @@ function md51(s) { // Converts the string to UTF-8 "bytes" s = unescape(encodeURI(s)); - var n = s.length, + let n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= s.length; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } s = s.substring(i - 64); - var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; for (i = 0; i < s.length; i++) { tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3); } @@ -431,7 +431,7 @@ function md51(s) { function md5blk(s) { /* I figured global was faster. */ - var md5blks = [], + let md5blks = [], i; /* Andy King said do it this way. */ for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = @@ -443,10 +443,10 @@ function md5blk(s) { return md5blks; } -var hex_chr = "0123456789abcdef".split(""); +let hex_chr = "0123456789abcdef".split(""); function rhex(n) { - var s = "", + let s = "", j = 0; for (; j < 4; j++) { s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; @@ -455,7 +455,7 @@ function rhex(n) { } function hex(x) { - for (var i = 0; i < x.length; i++) { + for (let i = 0; i < x.length; i++) { x[i] = rhex(x[i]); } return x.join(""); diff --git a/plugins/poll/assets/javascripts/lib/even-round.js.es6 b/plugins/poll/assets/javascripts/lib/even-round.js.es6 index 93c1226b7a..cd8bf34593 100644 --- a/plugins/poll/assets/javascripts/lib/even-round.js.es6 +++ b/plugins/poll/assets/javascripts/lib/even-round.js.es6 @@ -4,7 +4,7 @@ function sumsUpTo100(percentages) { } export default function (percentages) { - var decimals = percentages.map((a) => a % 1); + let decimals = percentages.map((a) => a % 1); const sumOfDecimals = Math.ceil(decimals.reduce((a, b) => a + b)); // compensate error by adding 1 to n items with the greatest decimal part for (let i = 0, max = decimals.length; i < sumOfDecimals && i < max; i++) { diff --git a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 index fa98363173..5972107295 100644 --- a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 +++ b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 @@ -553,7 +553,7 @@ function pieChartConfig(data, labels, opts = {}) { } function stripHtml(html) { - var doc = new DOMParser().parseFromString(html, "text/html"); + let doc = new DOMParser().parseFromString(html, "text/html"); return doc.body.textContent || ""; } diff --git a/plugins/poll/test/javascripts/acceptance/poll-in-reply-history-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-in-reply-history-test.js.es6 new file mode 100644 index 0000000000..1959caab88 --- /dev/null +++ b/plugins/poll/test/javascripts/acceptance/poll-in-reply-history-test.js.es6 @@ -0,0 +1,444 @@ +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; +import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer"; + +acceptance("Poll in a post reply history", function (needs) { + needs.user(); + needs.settings({ poll_enabled: true }); + needs.hooks.beforeEach(() => { + clearPopupMenuOptionsCallback(); + }); + + needs.pretender((server, helper) => { + server.get("/t/topic_with_poll_in_post_reply_history.json", () => { + return helper.response({ + post_stream: { + posts: [ + { + id: 82, + name: null, + username: "admin1", + avatar_template: + "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + created_at: "2021-01-25T13:08:27.385Z", + cooked: "

    A reply to the poll.

    ", + post_number: 4, + post_type: 1, + updated_at: "2021-01-25T13:08:27.385Z", + reply_count: 0, + reply_to_post_number: 2, + quote_count: 0, + incoming_link_count: 0, + reads: 1, + readers_count: 0, + score: 0.2, + yours: true, + topic_id: 25, + topic_slug: "topic-with-a-poll-in-a-post-reply-history", + display_username: null, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 1, + can_edit: true, + can_delete: true, + can_recover: false, + can_wiki: true, + read: true, + user_title: null, + reply_to_user: { + username: "admin1", + avatar_template: + "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + }, + bookmarked: false, + actions_summary: [ + { + id: 3, + can_act: true, + }, + { + id: 4, + can_act: true, + }, + { + id: 8, + can_act: true, + }, + { + id: 7, + can_act: true, + }, + ], + moderator: false, + admin: true, + staff: true, + user_id: 3, + hidden: false, + trust_level: 1, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + reviewable_id: 0, + reviewable_score_count: 0, + reviewable_score_pending_count: 0, + }, + ], + stream: [82], + }, + timeline_lookup: [[1, 0]], + suggested_topics: [ + { + id: 7, + title: "Welcome to Discourse", + fancy_title: "Welcome to Discourse", + slug: "welcome-to-discourse", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2021-01-07T15:36:44.707Z", + last_posted_at: "2021-01-07T15:36:44.750Z", + bumped: true, + bumped_at: "2021-01-07T15:36:44.750Z", + archetype: "regular", + unseen: false, + pinned: true, + unpinned: null, + excerpt: + "The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It’s important! Edit this into a brief description of your community: Who is it for? What can they fi…", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + like_count: 0, + views: 1, + category_id: 1, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: -1, + username: "system", + name: "system", + avatar_template: + "http://localhost:3000/images/discourse-logo-sketch-small.png", + }, + }, + ], + }, + { + id: 20, + title: "Polls testing. Just one poll in the comment", + fancy_title: "Polls testing. Just one poll in the comment", + slug: "polls-testing-just-one-poll-in-the-comment", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: null, + created_at: "2021-01-21T09:21:35.102Z", + last_posted_at: "2021-01-22T09:35:33.543Z", + bumped: true, + bumped_at: "2021-01-22T09:35:33.543Z", + archetype: "regular", + unseen: false, + last_read_post_number: 3, + unread: 0, + new_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 2, + bookmarked: false, + liked: false, + like_count: 0, + views: 3, + category_id: 1, + featured_link: null, + posters: [ + { + extras: null, + description: "Original Poster", + user: { + id: 2, + username: "andrey1", + name: "andrey1", + avatar_template: + "/letter_avatar_proxy/v4/letter/a/c0e974/{size}.png", + }, + }, + { + extras: "latest", + description: "Most Recent Poster", + user: { + id: 3, + username: "admin1", + name: null, + avatar_template: + "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + }, + }, + ], + }, + { + id: 22, + title: "Polls testing. The whole test", + fancy_title: "Polls testing. The whole test", + slug: "polls-testing-the-whole-test", + posts_count: 12, + reply_count: 8, + highest_post_number: 12, + image_url: null, + created_at: "2021-01-21T09:55:20.135Z", + last_posted_at: "2021-01-22T11:59:31.561Z", + bumped: true, + bumped_at: "2021-01-22T11:59:31.561Z", + archetype: "regular", + unseen: false, + last_read_post_number: 12, + unread: 0, + new_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 2, + bookmarked: false, + liked: false, + like_count: 0, + views: 4, + category_id: 1, + featured_link: null, + posters: [ + { + extras: null, + description: "Original Poster", + user: { + id: 2, + username: "andrey1", + name: "andrey1", + avatar_template: + "/letter_avatar_proxy/v4/letter/a/c0e974/{size}.png", + }, + }, + { + extras: "latest", + description: "Most Recent Poster", + user: { + id: 3, + username: "admin1", + name: null, + avatar_template: + "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + }, + }, + ], + }, + ], + id: 25, + title: "Topic with a poll in a post reply history", + fancy_title: "Topic with a poll in a post reply history", + posts_count: 4, + created_at: "2021-01-25T13:07:31.670Z", + views: 2, + reply_count: 2, + like_count: 0, + last_posted_at: "2021-01-25T13:08:27.385Z", + visible: true, + closed: false, + archived: false, + has_summary: false, + archetype: "regular", + slug: "topic-with-a-poll-in-a-post-reply-history", + category_id: 1, + word_count: 25, + deleted_at: null, + user_id: 3, + featured_link: null, + pinned_globally: false, + pinned_at: null, + pinned_until: null, + image_url: null, + slow_mode_seconds: 0, + draft: null, + draft_key: "topic_25", + draft_sequence: 4, + posted: true, + unpinned: null, + pinned: false, + current_post_number: 4, + highest_post_number: 4, + last_read_post_number: 4, + last_read_post_id: 82, + deleted_by: null, + has_deleted: false, + actions_summary: [ + { + id: 4, + count: 0, + hidden: false, + can_act: true, + }, + { + id: 8, + count: 0, + hidden: false, + can_act: true, + }, + { + id: 7, + count: 0, + hidden: false, + can_act: true, + }, + ], + chunk_size: 20, + bookmarked: false, + topic_timer: null, + message_bus_last_id: 4, + participant_count: 1, + show_read_indicator: false, + thumbnails: null, + details: { + can_edit: true, + notification_level: 3, + notifications_reason_id: 1, + can_move_posts: true, + can_delete: true, + can_remove_allowed_users: true, + can_invite_to: true, + can_invite_via_email: true, + can_create_post: true, + can_reply_as_new_topic: true, + can_flag_topic: true, + can_convert_topic: true, + can_review_topic: true, + can_close_topic: true, + can_archive_topic: true, + can_split_merge_topic: true, + can_edit_staff_notes: true, + can_toggle_topic_visibility: true, + can_moderate_category: true, + can_remove_self_id: 3, + participants: [ + { + id: 3, + username: "admin1", + name: null, + avatar_template: + "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + post_count: 4, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_color: null, + primary_group_flair_bg_color: null, + }, + ], + created_by: { + id: 3, + username: "admin1", + name: null, + avatar_template: + "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + }, + last_poster: { + id: 3, + username: "admin1", + name: null, + avatar_template: + "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + }, + }, + }); + }); + + server.get("/posts/82/reply-history", () => { + return helper.response([ + { + id: 80, + name: null, + username: "admin1", + avatar_template: "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + created_at: "2021-01-25T13:07:58.995Z", + cooked: + '

    The poll:

    \n
    \n
    \n
    \n
      \n
    • Option A
    • \n
    • Option B
    • \n
    \n
    \n
    \n

    \n0\nvoters\n

    \n
    \n
    \n
    ', + post_number: 2, + post_type: 1, + updated_at: "2021-01-25T13:07:58.995Z", + reply_count: 2, + reply_to_post_number: null, + quote_count: 0, + incoming_link_count: 0, + reads: 1, + readers_count: 0, + score: 10.2, + yours: false, + topic_id: 25, + topic_slug: "topic-with-a-poll-in-a-post-reply-history", + display_username: null, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 1, + can_edit: false, + can_delete: false, + can_recover: false, + can_wiki: false, + user_title: null, + bookmarked: false, + actions_summary: [], + moderator: false, + admin: true, + staff: true, + user_id: 3, + hidden: false, + trust_level: 1, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + polls: [ + { + name: "poll", + type: "regular", + status: "open", + results: "always", + options: [ + { + id: "5b8ee5ba2a43e258f93dbef9264bf1ad", + html: "Option A", + votes: 0, + }, + { + id: "6872645f5d8ef2311883617a3a7d381b", + html: "Option B", + votes: 0, + }, + ], + voters: 0, + chart_type: "bar", + title: null, + }, + ], + }, + ]); + }); + }); + + test("renders and extends", async function (assert) { + await visit("/t/-/topic_with_poll_in_post_reply_history"); + await click(".reply-to-tab"); + assert.ok(exists(".poll"), "poll is rendered"); + assert.ok(exists(".poll-buttons"), "poll is extended"); + }); +}); diff --git a/plugins/poll/test/javascripts/acceptance/poll-quote-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-quote-test.js.es6 index 45b93f47c5..846f97c4a1 100644 --- a/plugins/poll/test/javascripts/acceptance/poll-quote-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/poll-quote-test.js.es6 @@ -9,257 +9,6 @@ acceptance("Poll quote", function (needs) { }); needs.pretender((server, helper) => { - server.get("/t/topic_with_two_polls.json", () => { - return helper.response({ - post_stream: { - posts: [ - { - id: 133, - name: null, - username: "bianca", - avatar_template: - "/letter_avatar_proxy/v4/letter/b/3be4f8/{size}.png", - created_at: "2020-08-17T12:05:24.577Z", - cooked: - '
    \n
    \n
    \n
      \n
    • Alpha
    • \n
    • Beta
    • \n
    \n
    \n
    \n

    \n0\nvoters\n

    \n
    \n
    \n
    \n
    \n
    \n
    \n
      \n
    • First
    • \n
    • Second
    • \n
    \n
    \n
    \n

    \n0\nvoters\n

    \n
    \n
    \n
    ', - post_number: 1, - post_type: 1, - updated_at: "2020-08-17T12:05:24.577Z", - reply_count: 0, - reply_to_post_number: null, - quote_count: 0, - incoming_link_count: 0, - reads: 1, - readers_count: 0, - score: 0, - yours: true, - topic_id: 130, - topic_slug: "topic-with-two-polls", - display_username: null, - primary_group_name: null, - primary_group_flair_url: null, - primary_group_flair_bg_color: null, - primary_group_flair_color: null, - version: 1, - can_edit: true, - can_delete: false, - can_recover: false, - can_wiki: true, - link_counts: [ - { - url: - "http://forum.example.com/t/topic-with-two-quoted-polls/129", - internal: true, - reflection: true, - title: "Topic with two quoted polls", - clicks: 0, - }, - ], - read: true, - user_title: "Tester", - title_is_group: false, - bookmarked: false, - actions_summary: [ - { - id: 3, - can_act: true, - }, - { - id: 4, - can_act: true, - }, - { - id: 8, - can_act: true, - }, - { - id: 7, - can_act: true, - }, - ], - moderator: false, - admin: true, - staff: true, - user_id: 1, - hidden: false, - trust_level: 0, - deleted_at: null, - user_deleted: false, - edit_reason: null, - can_view_edit_history: true, - wiki: false, - reviewable_id: 0, - reviewable_score_count: 0, - reviewable_score_pending_count: 0, - calendar_details: [], - polls: [ - { - name: "poll1", - type: "regular", - status: "open", - results: "always", - options: [ - { - id: "bf48cd4958a17180e2a298e246988f94", - html: "Alpha", - votes: 0, - }, - { - id: "c19aa835729ab0413a84a2c9850c4005", - html: "Beta", - votes: 0, - }, - ], - voters: 0, - chart_type: "bar", - }, - { - name: "poll2", - type: "regular", - status: "open", - results: "always", - options: [ - { - id: "def034c6770c6fd3754c054ef9ec4721", - html: "First", - votes: 0, - }, - { - id: "e0f55d1a981683789bec2a0b05eb70ef", - html: "Second", - votes: 0, - }, - ], - voters: 0, - chart_type: "bar", - }, - ], - }, - ], - stream: [133], - }, - timeline_lookup: [[1, 0]], - suggested_topics: [], - tags: [], - id: 130, - title: "Topic with two polls", - fancy_title: "Topic with two polls", - posts_count: 1, - created_at: "2020-08-17T12:05:24.500Z", - views: 1, - reply_count: 0, - like_count: 0, - last_posted_at: "2020-08-17T12:05:24.577Z", - visible: true, - closed: false, - archived: false, - has_summary: false, - archetype: "regular", - slug: "topic-with-two-polls", - category_id: 1, - word_count: 24, - deleted_at: null, - user_id: 1, - featured_link: null, - pinned_globally: false, - pinned_at: null, - pinned_until: null, - image_url: null, - draft: null, - draft_key: "topic_130", - draft_sequence: 0, - posted: true, - unpinned: null, - pinned: false, - current_post_number: 1, - highest_post_number: 1, - last_read_post_number: 1, - last_read_post_id: 133, - deleted_by: null, - has_deleted: false, - actions_summary: [ - { - id: 4, - count: 0, - hidden: false, - can_act: true, - }, - { - id: 8, - count: 0, - hidden: false, - can_act: true, - }, - { - id: 7, - count: 0, - hidden: false, - can_act: true, - }, - ], - chunk_size: 20, - bookmarked: false, - topic_timer: null, - message_bus_last_id: 1, - participant_count: 1, - queued_posts_count: 0, - show_read_indicator: false, - thumbnails: null, - can_vote: false, - vote_count: null, - user_voted: false, - details: { - notification_level: 3, - notifications_reason_id: 1, - can_move_posts: true, - can_edit: true, - can_delete: true, - can_remove_allowed_users: true, - can_invite_to: true, - can_invite_via_email: true, - can_create_post: true, - can_reply_as_new_topic: true, - can_flag_topic: true, - can_convert_topic: true, - can_review_topic: true, - can_close_topic: true, - can_archive_topic: true, - can_split_merge_topic: true, - can_edit_staff_notes: true, - can_remove_self_id: 1, - participants: [ - { - id: 1, - username: "bianca", - name: null, - avatar_template: - "/letter_avatar_proxy/v4/letter/b/3be4f8/{size}.png", - post_count: 1, - primary_group_name: null, - primary_group_flair_url: null, - primary_group_flair_color: null, - primary_group_flair_bg_color: null, - }, - ], - created_by: { - id: 1, - username: "bianca", - name: null, - avatar_template: - "/letter_avatar_proxy/v4/letter/b/3be4f8/{size}.png", - }, - last_poster: { - id: 1, - username: "bianca", - name: null, - avatar_template: - "/letter_avatar_proxy/v4/letter/b/3be4f8/{size}.png", - }, - }, - pending_posts: [], - }); - }); - server.get("/posts/by_number/130/1", () => { return helper.response({ id: 133, @@ -675,9 +424,10 @@ acceptance("Poll quote", function (needs) { }); }); - test("Quoted polls", async function (assert) { + test("renders and extends", async function (assert) { await visit("/t/-/topic_with_two_quoted_polls"); await click(".quote-controls"); - assert.equal(queryAll(".poll").length, 2); + assert.equal(queryAll(".poll").length, 2, "polls are rendered"); + assert.equal(queryAll(".poll-buttons").length, 2, "polls are extended"); }); }); diff --git a/plugins/styleguide/assets/javascripts/discourse/components/styleguide-example.js.es6 b/plugins/styleguide/assets/javascripts/discourse/components/styleguide-example.js.es6 index b5fa4460ea..3c71339b78 100644 --- a/plugins/styleguide/assets/javascripts/discourse/components/styleguide-example.js.es6 +++ b/plugins/styleguide/assets/javascripts/discourse/components/styleguide-example.js.es6 @@ -1,4 +1,10 @@ export default Ember.Component.extend({ tagName: "section", classNames: ["styleguide-example"], + value: null, + + init() { + this._super(...arguments); + this.value = this.initialValue; + }, }); diff --git a/plugins/styleguide/assets/javascripts/discourse/components/styleguide-icons.js.es6 b/plugins/styleguide/assets/javascripts/discourse/components/styleguide-icons.js.es6 index a11836c7a3..ab2be7eb7d 100644 --- a/plugins/styleguide/assets/javascripts/discourse/components/styleguide-icons.js.es6 +++ b/plugins/styleguide/assets/javascripts/discourse/components/styleguide-icons.js.es6 @@ -1,21 +1,20 @@ -import { later } from "@ember/runloop"; +import { afterRender } from "discourse-common/utils/decorators"; export default Ember.Component.extend({ tagName: "section", classNames: ["styleguide-icons"], - iconIDs: [], + iconIds: [], - didInsertElement() { + init() { this._super(...arguments); + this.setIconIds(); + }, - later(() => { - let IDs = $("#svg-sprites symbol") - .map(function () { - return this.id; - }) - .get(); + @afterRender + setIconIds() { + let symbols = document.querySelectorAll("#svg-sprites symbol"); + let ids = Array.from(symbols).mapBy("id"); - this.set("iconIDs", IDs); - }, 2000); + this.set("iconIds", ids); }, }); diff --git a/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js.es6 b/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js.es6 index f334cdbbda..c036e4c366 100644 --- a/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js.es6 +++ b/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js.es6 @@ -1,4 +1,3 @@ -import EmberObject from "@ember/object"; import NavItem from "discourse/models/nav-item"; let topicId = 2000000; @@ -48,7 +47,7 @@ export function createData(store) { website_name: "My Website is Discourse", location: "Toronto", suspend_reason: "Some reason", - displayGroups: [{ name: "Group 1" }, { name: "Group 2" }], + groups: [{ name: "Group 1" }, { name: "Group 2" }], created_at: moment().subtract(10, "days"), last_posted_at: moment().subtract(3, "days"), last_seen_at: moment().subtract(1, "days"), @@ -56,7 +55,7 @@ export function createData(store) { invited_by: { username: "user_2", }, - trustLevel: { name: "Dummy" }, + trust_level: 1, publicUserFields: [ { field: { @@ -94,13 +93,13 @@ export function createData(store) { { id: topicId, title: `Example Topic Title ${topicId}`, - fancyTitle: `Example Topic Title ${topicId}`, + fancy_title: `Example Topic Title ${topicId}`, slug: `example-topic-title-${topicId}`, posts_count: ((topicId * 1234) % 100) + 1, views: ((topicId * 123) % 1000) + 1, like_count: topicId % 3, created_at: `2017-03-${topicId}`, - invisible: false, + visible: true, posters: [ { extras: "latest", user }, { user: createUser() }, @@ -115,25 +114,25 @@ export function createData(store) { }; let topic = createTopic({ tags: ["example", "apple"] }); + topic.details.updateFromJson({ + can_create_post: true, + can_invite_to: false, + can_delete: false, + can_close_topic: false, + }); topic.setProperties({ - details: EmberObject.create({ - can_create_post: true, - can_invite_to: false, - can_delete: false, - can_close_topic: false, - }), - category: categories[0], + category_id: categories[0].id, suggested_topics: [topic, topic, topic], }); - let invisibleTopic = createTopic({ invisible: true }); + let invisibleTopic = createTopic({ visible: false }); let closedTopic = createTopic({ closed: true }); - closedTopic.set("category", categories[1]); + closedTopic.set("category_id", categories[1].id); let archivedTopic = createTopic({ archived: true }); let pinnedTopic = createTopic({ pinned: true }); pinnedTopic.set("clearPin", () => pinnedTopic.set("pinned", "unpinned")); pinnedTopic.set("rePin", () => pinnedTopic.set("pinned", "pinned")); - pinnedTopic.set("category", categories[2]); + pinnedTopic.set("category_id", categories[2].id); let unpinnedTopic = createTopic({ unpinned: true }); let warningTopic = createTopic({ is_warning: true }); @@ -220,7 +219,7 @@ export function createData(store) { navItems: ["latest", "categories", "top"].map((name) => { let item = NavItem.fromText(name); - item.set("href", "#"); + // item.set("href", "#"); if (name === "categories") { item.set("styleGuideActive", true); @@ -256,8 +255,6 @@ export function createData(store) { topicTimerUpdateDate: "2017-10-18 18:00", - categoryNames: categories.map((c) => c.name), - groups: [ { name: "staff", id: 1, automatic: false }, { name: "lounge", id: 2, automatic: true }, diff --git a/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-example.hbs b/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-example.hbs index 9a98389ca3..193911014c 100644 --- a/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-example.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-example.hbs @@ -1,3 +1,3 @@
    {{title}}
    -
    {{yield}}
    +
    {{yield this.value}}
    diff --git a/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-icons.hbs b/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-icons.hbs index 996790bb48..4b0bf42fab 100644 --- a/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-icons.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/templates/components/styleguide-icons.hbs @@ -1,6 +1,6 @@ -{{#each iconIDs as |id|}} +{{#each iconIds as |id|}}
    - {{ d-icon id }} + {{d-icon id}} {{id}}
    {{/each}} diff --git a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/dropdowns.hbs b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/dropdowns.hbs index 15508cddf6..0089e62841 100644 --- a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/dropdowns.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/dropdowns.hbs @@ -1,35 +1,29 @@ -{{#styleguide-example title="combo-box"}} - {{combo-box content=dummy.options}} +{{#styleguide-example title="combo-box" initialValue=dummy.options.[0].name as |value|}} + {{combo-box content=dummy.options value=value onChange=(fn (mut value))}} {{/styleguide-example}} -{{#styleguide-example title="filterable combo-box"}} - {{combo-box content=dummy.categoryNames filterable=true}} +{{#styleguide-example title="filterable combo-box" initialValue=dummy.categories.[0].name as |value|}} + {{combo-box content=dummy.categories value=value options=(hash filterable=true) onChange=(fn (mut value))}} {{/styleguide-example}} -{{#styleguide-example title="combo-box with a default state"}} - {{combo-box content=dummy.options none="category.none"}} +{{#styleguide-example title="combo-box with a default state" initialValue=dummy.options.[0].name as |value|}} + {{combo-box content=dummy.options value=value options=(hash none="category.none") onChange=(fn (mut value))}} {{/styleguide-example}} -{{#styleguide-example title="combo-box clearable"}} - {{combo-box content=dummy.options none="category.none" clearable=true}} +{{#styleguide-example title="combo-box clearable" initialValue=dummy.options.[0].name as |value|}} + {{combo-box content=dummy.options clearable=true value=value options=(hash none="category.none") onChange=(fn (mut value))}} {{/styleguide-example}} -{{#styleguide-example title="topic-notifications-options"}} - {{topic-notifications-options topic=dummy.topic}} -{{/styleguide-example}} - -{{#styleguide-example title="topic-notifications-button"}} - +{{#styleguide-example title="topic-notifications-options" initialValue=1 as |value|}} + {{topic-notifications-options topic=dummy.topic value=value onChange=(fn (mut value))}} {{/styleguide-example}} {{#styleguide-example title="topic-footer-mobile-dropdown"}} {{topic-footer-mobile-dropdown topic=dummy.topic}} {{/styleguide-example}} -{{#styleguide-example title="category-chooser"}} - {{category-chooser}} +{{#styleguide-example title="category-chooser" initialValue=categories.[0].name as |value|}} + {{category-chooser value=value onChange=(fn (mut value))}} {{/styleguide-example}} {{#styleguide-example title="pinned-button"}} @@ -41,19 +35,19 @@ {{/styleguide-example}} {{#styleguide-example title="categories-admin-dropdown"}} - {{categories-admin-dropdown}} + {{categories-admin-dropdown onChange=(action "dummy")}} {{/styleguide-example}} {{#styleguide-example title="category-notifications-button"}} - {{category-notifications-button category=dummy.categories.[0]}} + {{category-notifications-button category=dummy.categories.[0] value=1 onChange=(action "dummy")}} {{/styleguide-example}} {{#styleguide-example title="notifications-button"}} - {{notifications-button i18nPrefix="groups.notifications" value=1}} + {{notifications-button options=(hash i18nPrefix="groups.notifications") value=2 onChange=(action "dummy")}} {{/styleguide-example}} {{#styleguide-example title="dropdown-select-box"}} - {{dropdown-select-box content=dummy.options}} + {{dropdown-select-box content=dummy.options onChange=(action "dummy")}} {{/styleguide-example}} {{#styleguide-example title="future-date-input-selector"}} @@ -63,22 +57,22 @@ input=dummy.topicTimerUpdateDate includeWeekend=true includeForever=true - none="topic.auto_update_input.none" + options=(hash none="topic.auto_update_input.none") }} {{/styleguide-example}} {{#styleguide-example title="multi-select"}} - {{multi-select none="test.none" content=dummy.options}} + {{multi-select content=dummy.options options=(hash none="test.none") onChange=(action "dummy")}} {{/styleguide-example}} -{{#styleguide-example title="admin-group-selector"}} - {{admin-group-selector selected=dummy.selectedGroups content=dummy.groups}} +{{#styleguide-example title="admin group-chooser"}} + {{group-chooser selected=dummy.selectedGroups content=dummy.groups onChange=(action "dummy")}} {{/styleguide-example}} {{#styleguide-example title="list-setting"}} - {{list-setting settingValue=dummy.settings}} + {{list-setting settingValue=dummy.settings onChange=(action "dummy")}} {{/styleguide-example}} {{#styleguide-example title="list-setting with colors"}} - {{list-setting settingValue=dummy.colors nameProperty="color"}} + {{list-setting settingValue=dummy.colors nameProperty="color" onChange=(action "dummy")}} {{/styleguide-example}} diff --git a/plugins/styleguide/assets/stylesheets/styleguide.scss b/plugins/styleguide/assets/stylesheets/styleguide.scss index 942a1d9496..44c9834042 100644 --- a/plugins/styleguide/assets/stylesheets/styleguide.scss +++ b/plugins/styleguide/assets/stylesheets/styleguide.scss @@ -11,11 +11,13 @@ } .styleguide-menu { + margin-top: 1em; flex: 1 0 0; ul { list-style: none; - margin-bottom: 2em; + margin-bottom: 1.5em; + margin-left: 0; li.styleguide-heading { color: var(--primary-medium, $primary-medium); @@ -45,9 +47,9 @@ .styleguide-section { .section-title { - font-size: $font-up-6; + font-size: $font-up-5; font-weight: normal; - margin-bottom: 1em; + margin-bottom: 0.5em; } .section-description { @@ -65,14 +67,15 @@ .styleguide-example { .example-title { color: var(--primary-medium, $primary-medium); - font-size: 0.8em; + font-size: $font-0; border-bottom: 1px solid var(--primary-low, $primary-low); - margin-bottom: 0.8em; + margin-bottom: 1em; } .rendered { width: 100%; } + margin-bottom: 2em; } } diff --git a/plugins/styleguide/config/locales/client.hu.yml b/plugins/styleguide/config/locales/client.hu.yml index 9dd03b94fd..9ca0c76aee 100644 --- a/plugins/styleguide/config/locales/client.hu.yml +++ b/plugins/styleguide/config/locales/client.hu.yml @@ -35,6 +35,8 @@ hu: title: "Legördülők" categories: title: "Kategóriák" + bread_crumbs: + title: "Kenyérmorzsák" navigation: title: "Navigáció" navigation_bar: @@ -51,15 +53,35 @@ hu: title: "Témaállapotok" topic_list: title: "Témalista" + basic_topic_list: + title: "Alapvető témakörlista" footer_message: title: "Lábléc üzenet" + signup_cta: + title: "Regisztrációs felhívás" + topic_timer_info: + title: "Témaidőzítők" + topic_footer_buttons: + title: "Téma láblécgombjai" + topic_notifications: + title: "Témaértesítések" post: title: "Bejegyzés" topic_map: title: "Téma térkép" + site_header: + title: "Webhelyfejléc" suggested_topics: title: "Ajánlott témák" post_menu: title: "Bejegyzés menü" + modal: + title: "Párbeszédablak" + header: "Párbeszédablak címe" + footer: "Párbeszédablak lábléce" + user_about: + title: "Felhasználói névjegydoboz" header_icons: title: "Fejléc ikonok" + spinners: + title: "Forgók" diff --git a/plugins/styleguide/config/locales/server.hu.yml b/plugins/styleguide/config/locales/server.hu.yml index 0a2b3ed09f..c24ee08667 100644 --- a/plugins/styleguide/config/locales/server.hu.yml +++ b/plugins/styleguide/config/locales/server.hu.yml @@ -4,4 +4,7 @@ # To work with us on translations, join this project: # https://translate.discourse.org/ -hu: +hu: + site_settings: + styleguide_enabled: "A „/styleguide” elérési út engedélyezése a Discourse stílusmódosításainak megkönnyítéséhez" + styleguide_admin_only: "Az adminisztrátorokra korlátozza a stílusútmutató láthatóságát" diff --git a/public/422.en_GB.html b/public/422.en_GB.html index 244e10b089..e1dd1706bb 100644 --- a/public/422.en_GB.html +++ b/public/422.en_GB.html @@ -20,7 +20,7 @@
    -

    The change you wanted was rejected.

    +

    The change you wanted has been rejected.

    Maybe you tried to change something you didn't have access to.

    diff --git a/spec/components/discourse_updates_spec.rb b/spec/components/discourse_updates_spec.rb index 4b83510c24..41df256863 100644 --- a/spec/components/discourse_updates_spec.rb +++ b/spec/components/discourse_updates_spec.rb @@ -144,4 +144,92 @@ describe DiscourseUpdates do include_examples "when last_installed_version is old" end end + + context 'new features' do + fab!(:admin) { Fabricate(:admin) } + fab!(:admin2) { Fabricate(:admin) } + let!(:last_item_date) { 5.minutes.ago } + let!(:sample_features) { [ + { "emoji" => "🤾", "title" => "Super Fruits", "description" => "Taste explosion!", "created_at" => 40.minutes.ago }, + { "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Magic legumes!", "created_at" => 15.minutes.ago }, + { "emoji" => "🤾", "title" => "Quality Veggies", "description" => "Green goodness!", "created_at" => last_item_date }, + ] } + + before(:each) do + Discourse.redis.del "new_features_last_seen_user_#{admin.id}" + Discourse.redis.del "new_features_last_seen_user_#{admin2.id}" + Discourse.redis.del "new_features" + + Discourse.redis.set('new_features', MultiJson.dump(sample_features)) + end + + it 'returns all items on the first run' do + result = DiscourseUpdates.unseen_new_features(admin.id) + + expect(result.length).to eq(3) + expect(result[2]["title"]).to eq("Super Fruits") + end + + it 'returns only unseen items by user' do + DiscourseUpdates.stubs(:new_features_last_seen).with(admin.id).returns(10.minutes.ago) + DiscourseUpdates.stubs(:new_features_last_seen).with(admin2.id).returns(30.minutes.ago) + + result = DiscourseUpdates.unseen_new_features(admin.id) + expect(result.length).to eq(1) + expect(result[0]["title"]).to eq("Quality Veggies") + + result2 = DiscourseUpdates.unseen_new_features(admin2.id) + expect(result2.length).to eq(2) + expect(result2[0]["title"]).to eq("Quality Veggies") + expect(result2[1]["title"]).to eq("Fancy Legumes") + end + + it 'can mark features as seen for a given user' do + expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_present + + DiscourseUpdates.mark_new_features_as_seen(admin.id) + expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_empty + + # doesn't affect another user + expect(DiscourseUpdates.unseen_new_features(admin2.id)).to be_present + + end + + it 'correctly sees newly added features as unseen' do + DiscourseUpdates.mark_new_features_as_seen(admin.id) + expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_empty + expect(DiscourseUpdates.new_features_last_seen(admin.id)).to be_within(1.second).of (last_item_date) + + updated_features = [ + { "emoji" => "🤾", "title" => "Brand New Item", "created_at" => 2.minutes.ago } + ] + updated_features += sample_features + + Discourse.redis.set('new_features', MultiJson.dump(updated_features)) + + result = DiscourseUpdates.unseen_new_features(admin.id) + expect(result.length).to eq(1) + expect(result[0]["title"]).to eq("Brand New Item") + end + + it 'correctly shows features by Discourse version' do + features_with_versions = [ + { "emoji" => "🤾", "title" => "Bells", "created_at" => 40.minutes.ago }, + { "emoji" => "🙈", "title" => "Whistles", "created_at" => 20.minutes.ago, discourse_version: "2.6.0.beta1" }, + { "emoji" => "🙈", "title" => "Confetti", "created_at" => 15.minutes.ago, discourse_version: "2.7.0.beta2" }, + { "emoji" => "🤾", "title" => "Not shown yet", "created_at" => 10.minutes.ago, discourse_version: "2.7.0.beta5" }, + { "emoji" => "🤾", "title" => "Not shown yet (beta < stable)", "created_at" => 10.minutes.ago, discourse_version: "2.7.0" }, + { "emoji" => "🤾", "title" => "Ignore invalid version", "created_at" => 10.minutes.ago, discourse_version: "invalid-version" }, + ] + + Discourse.redis.set('new_features', MultiJson.dump(features_with_versions)) + DiscourseUpdates.stubs(:last_installed_version).returns("2.7.0.beta2") + result = DiscourseUpdates.unseen_new_features(admin.id) + + expect(result.length).to eq(3) + expect(result[0]["title"]).to eq("Confetti") + expect(result[1]["title"]).to eq("Whistles") + expect(result[2]["title"]).to eq("Bells") + end + end end diff --git a/spec/components/plugin/metadata_spec.rb b/spec/components/plugin/metadata_spec.rb index fe838d8c7b..6dd0ac000e 100644 --- a/spec/components/plugin/metadata_spec.rb +++ b/spec/components/plugin/metadata_spec.rb @@ -10,6 +10,7 @@ describe Plugin::Metadata do # about: about: my plugin # version: 0.1 # authors: Frank Zappa +# contact emails: frankz@example.com # url: http://discourse.org # required version: 1.3.0beta6+48 @@ -20,6 +21,7 @@ TEXT expect(metadata.about).to eq("about: my plugin") expect(metadata.version).to eq("0.1") expect(metadata.authors).to eq("Frank Zappa") + expect(metadata.contact_emails).to eq("frankz@example.com") expect(metadata.url).to eq("http://discourse.org") expect(metadata.required_version).to eq("1.3.0beta6+48") end diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index b34e2c4080..16a583a2a5 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -961,7 +961,7 @@ describe Search do it 'can find posts with tags' do # we got to make this index (it is deferred) - Jobs::ReindexSearch.new.rebuild_problem_posts + Jobs::ReindexSearch.new.rebuild_posts result = Search.execute(tag.name) expect(result.posts.length).to eq(1) @@ -977,7 +977,7 @@ describe Search do it 'can find posts with tag synonyms' do synonym = Fabricate(:tag, name: 'synonym', target_tag: tag) - Jobs::ReindexSearch.new.rebuild_problem_posts + Jobs::ReindexSearch.new.rebuild_posts result = Search.execute(synonym.name) expect(result.posts.length).to eq(1) end diff --git a/spec/components/timeline_lookup_spec.rb b/spec/components/timeline_lookup_spec.rb index 0ae24563f5..ea6a2bf8e6 100644 --- a/spec/components/timeline_lookup_spec.rb +++ b/spec/components/timeline_lookup_spec.rb @@ -25,16 +25,19 @@ describe TimelineLookup do input = (1..100).map { |i| [1000 + i, 100 - i] } result = TimelineLookup.build(input, 5) - expect(result.size).to eq(5) - expect(result).to eq([[1, 99], [21, 79], [41, 59], [61, 39], [81, 19]]) + # even if max_value is 5 we might get 6 (5 + 1) + # to ensure the last tuple is captured + expect(result).to eq([[1, 99], [21, 79], [41, 59], [61, 39], [81, 19], [input.size, input.last[1]]]) end it "respects an uneven `max_values` setting" do input = (1..100).map { |i| [1000 + i, 100 - i] } result = TimelineLookup.build(input, 3) - expect(result.size).to eq(3) - expect(result).to eq([[1, 99], [35, 65], [69, 31]]) + # even if max_value is 3 we might get 4 (3 + 1) + # to ensure the last tuple is captured + expect(result.size).to eq(4) + expect(result).to eq([[1, 99], [35, 65], [69, 31], [input.size, input.last[1]]]) end end diff --git a/spec/jobs/process_shelved_notifications_spec.rb b/spec/jobs/process_shelved_notifications_spec.rb index dd44c1ca6c..eca634330c 100644 --- a/spec/jobs/process_shelved_notifications_spec.rb +++ b/spec/jobs/process_shelved_notifications_spec.rb @@ -17,21 +17,21 @@ describe Jobs::ProcessShelvedNotifications do expect(DoNotDisturbTiming.find_by(id: past.id)).to eq(nil) end - it "does not process unprocessed notifications when the user is in DND" do + it "does not process shelved_notifications when the user is in DND" do user.do_not_disturb_timings.create(starts_at: 2.days.ago, ends_at: 2.days.from_now) notification = Notification.create(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1) - expect(notification.reload.processed).to eq(false) + expect(notification.shelved_notification).to be_present subject.execute({}) - expect(notification.reload.processed).to eq(false) + expect(notification.shelved_notification).to be_present end - it "processes unprocessed notifications when the user leaves DND" do + it "processes and destroys shelved_notifications when the user leaves DND" do user.do_not_disturb_timings.create(starts_at: 2.days.ago, ends_at: 2.days.from_now) notification = Notification.create(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1) user.do_not_disturb_timings.last.update(ends_at: 1.days.ago) - expect(notification.reload.processed).to eq(false) + expect(notification.shelved_notification).to be_present subject.execute({}) - expect(notification.reload.processed).to eq(true) + expect { notification.shelved_notification.reload }.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/jobs/reindex_search_spec.rb b/spec/jobs/reindex_search_spec.rb index b6132855e2..3128aeccb1 100644 --- a/spec/jobs/reindex_search_spec.rb +++ b/spec/jobs/reindex_search_spec.rb @@ -34,7 +34,7 @@ describe Jobs::ReindexSearch do end end - describe 'rebuild_problem_posts' do + describe 'rebuild_posts' do class FakeIndexer def self.index(post, force:) get_posts.push(post) @@ -59,10 +59,7 @@ describe Jobs::ReindexSearch do FakeIndexer.reset end - it ( - 'should not reindex posts that belong to a deleted topic ' \ - 'or have been trashed' - ) do + it "should not reindex posts that belong to a deleted topic or have been trashed" do post = Fabricate(:post) post2 = Fabricate(:post) post3 = Fabricate(:post) @@ -70,7 +67,7 @@ describe Jobs::ReindexSearch do post2.topic.trash! post3.trash! - subject.rebuild_problem_posts(indexer: FakeIndexer) + subject.rebuild_posts(indexer: FakeIndexer) expect(FakeIndexer.posts).to contain_exactly(post) end @@ -78,7 +75,7 @@ describe Jobs::ReindexSearch do it 'should not reindex posts with a developmental version' do post = Fabricate(:post, version: SearchIndexer::MIN_POST_REINDEX_VERSION + 1) - subject.rebuild_problem_posts(indexer: FakeIndexer) + subject.rebuild_posts(indexer: FakeIndexer) expect(FakeIndexer.posts).to eq([]) end @@ -94,7 +91,7 @@ describe Jobs::ReindexSearch do post2.save!(validate: false) - subject.rebuild_problem_posts(indexer: FakeIndexer) + subject.rebuild_posts(indexer: FakeIndexer) expect(FakeIndexer.posts).to contain_exactly(post) end @@ -107,9 +104,7 @@ describe Jobs::ReindexSearch do [topic, topic2].each { |t| SearchIndexer.index(t, force: true) } - freeze_time(described_class::CLEANUP_GRACE_PERIOD) do - topic.trash! - end + freeze_time(1.day.ago) { topic.trash! } expect { subject.execute({}) }.to change { TopicSearchData.count }.by(-1) expect(Topic.pluck(:id)).to contain_exactly(topic2.id) @@ -119,11 +114,7 @@ describe Jobs::ReindexSearch do ) end - it( - "should clean up post_search_data of posts with empty raw or posts from " \ - "trashed topics" - ) do - + it "should clean up post_search_data of posts with empty raw or posts from trashed topics" do post = Fabricate(:post) post2 = Fabricate(:post, post_type: Post.types[:small_action]) post2.raw = "" @@ -132,7 +123,7 @@ describe Jobs::ReindexSearch do post3.topic.trash! post4, post5, post6 = nil - freeze_time(described_class::CLEANUP_GRACE_PERIOD) do + freeze_time(1.day.ago) do post4 = Fabricate(:post) post4.topic.trash! diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index eab1cb4aa2..138ca3d76e 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -495,7 +495,6 @@ describe Notification do expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) notification = Notification.last - expect(notification.processed).to eq(true) expect(notification.notification_type).to eq(Notification.types[:membership_request_consolidated]) data = notification.data_hash @@ -516,7 +515,7 @@ describe Notification do notification = Notification.last expect(notification.notification_type).to eq(Notification.types[:membership_request_consolidated]) - expect(notification.processed).to eq(false) + expect(notification.shelved_notification).to be_present end end end @@ -543,18 +542,18 @@ describe Notification do end end - describe "processed" do + describe "do not disturb" do fab!(:user) { Fabricate(:user) } - it "is false after creation when the user is in do not disturb" do + it "creates a shelved_notification record when created while user is in DND" do user.do_not_disturb_timings.create(starts_at: Time.now, ends_at: 3.days.from_now) notification = Notification.create(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1) - expect(notification.processed).to be(false) + expect(notification.shelved_notification).to be_present end - it "is true after creation when the user isn't in do not disturb" do + it "doesn't create a shelved_notification record when created while user is isn't DND" do notification = Notification.create(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1) - expect(notification.processed).to be(true) + expect(notification.shelved_notification).to be_nil end end end diff --git a/spec/models/user_search_spec.rb b/spec/models/user_search_spec.rb index 5dad83c32b..c72abc8468 100644 --- a/spec/models/user_search_spec.rb +++ b/spec/models/user_search_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" describe UserSearch do @@ -11,170 +11,163 @@ describe UserSearch do fab!(:topic2) { Fabricate :topic } fab!(:topic3) { Fabricate :topic } fab!(:topic4) { Fabricate :topic } - fab!(:user1) { Fabricate :user, username: "mrb", name: "Michael Madsen", last_seen_at: 10.days.ago } - fab!(:user2) { Fabricate :user, username: "mrblue", name: "Eddie Code", last_seen_at: 9.days.ago } - fab!(:user3) { Fabricate :user, username: "mrorange", name: "Tim Roth", last_seen_at: 8.days.ago } - fab!(:user4) { Fabricate :user, username: "mrpink", name: "Steve Buscemi", last_seen_at: 7.days.ago } - fab!(:user5) { Fabricate :user, username: "mrbrown", name: "Quentin Tarantino", last_seen_at: 6.days.ago } - fab!(:user6) { Fabricate :user, username: "mrwhite", name: "Harvey Keitel", last_seen_at: 5.days.ago } - fab!(:inactive) { Fabricate :user, username: "Ghost", active: false } + fab!(:mr_b) { Fabricate :user, username: "mrb", name: "Michael Madsen", last_seen_at: 10.days.ago } + fab!(:mr_blue) { Fabricate :user, username: "mrblue", name: "Eddie Code", last_seen_at: 9.days.ago } + fab!(:mr_orange) { Fabricate :user, username: "mrorange", name: "Tim Roth", last_seen_at: 8.days.ago } + fab!(:mr_pink) { Fabricate :user, username: "mrpink", name: "Steve Buscemi", last_seen_at: 7.days.ago } + fab!(:mr_brown) { Fabricate :user, username: "mrbrown", name: "Quentin Tarantino", last_seen_at: 6.days.ago } + fab!(:mr_white) { Fabricate :user, username: "mrwhite", name: "Harvey Keitel", last_seen_at: 5.days.ago } + fab!(:inactive) { Fabricate :user, username: "Ghost", active: false } fab!(:admin) { Fabricate :admin, username: "theadmin" } fab!(:moderator) { Fabricate :moderator, username: "themod" } fab!(:staged) { Fabricate :staged } def search_for(*args) - UserSearch.new(*args).search + # mapping "username" so it's easier to debug + UserSearch.new(*args).search.map(&:username) end - context 'with a secure category' do - fab!(:group) { Fabricate :group } - fab!(:user) { Fabricate :user } - fab!(:searching_user) { Fabricate :user } + context "with a secure category" do + fab!(:user) { Fabricate(:user) } + fab!(:searching_user) { Fabricate(:user) } + fab!(:group) { Fabricate(:group) } + fab!(:category) { Fabricate(:category, read_restricted: true, user: user) } + before_all do + Fabricate(:category_group, category: category, group: group) + group.add(user) group.add(searching_user) group.save end - fab!(:category) { Fabricate(:category, - read_restricted: true, - user: user) - } - before_all { Fabricate(:category_group, category: category, group: group) } - it 'autocompletes with people in the category' do + it "autocompletes with people in the category" do results = search_for("", searching_user: searching_user, category_id: category.id) - - expect(user.username).to eq(results[0].username) - expect(results.length).to eq(1) + expect(results).to eq [user.username] end - it 'will lookup the category from the topic id' do + it "will lookup the category from the topic id" do topic = Fabricate(:topic, category: category) - _post = Fabricate(:post, user: topic.user, topic: topic) + Fabricate(:post, user: topic.user, topic: topic) results = search_for("", searching_user: searching_user, topic_id: topic.id) - expect(results.length).to eq(2) - - expect(results.map(&:username)).to contain_exactly( - user.username, topic.user.username - ) + expect(results).to eq [topic.user, user].map(&:username) end - it 'will raise an error if the user cannot see the category' do + it "will raise an error if the user cannot see the category" do expect do search_for("", searching_user: Fabricate(:user), category_id: category.id) end.to raise_error(Discourse::InvalidAccess) end - it 'will respect the group member visibility setting' do + it "will respect the group member visibility setting" do group.update(members_visibility_level: Group.visibility_levels[:owners]) results = search_for("", searching_user: searching_user, category_id: category.id) - expect(results.length).to eq(0) + expect(results).to be_blank group.add_owner(searching_user) results = search_for("", searching_user: searching_user, category_id: category.id) - expect(results.length).to eq(1) + expect(results).to eq [user.username] end end - it 'allows for correct underscore searching' do - Fabricate(:user, username: 'Under_Score') - Fabricate(:user, username: 'undertaker') + it "allows for correct underscore searching" do + Fabricate(:user, username: "undertaker") + under_score = Fabricate(:user, username: "Under_Score") - expect(search_for("under_sc").length).to eq(1) - expect(search_for("under_").length).to eq(1) + expect(search_for("under_sc")).to eq [under_score.username] + expect(search_for("under_")).to eq [under_score.username] end - it 'allows filtering by group' do + it "allows filtering by group" do + sam = Fabricate(:user, username: "sam") + Fabricate(:user, username: "samantha") + group = Fabricate(:group) - sam = Fabricate(:user, username: 'sam') - _samantha = Fabricate(:user, username: 'samantha') group.add(sam) results = search_for("sam", groups: [group]) - expect(results.count).to eq(1) + expect(results).to eq [sam.username] end - it 'allows filtering by multiple groups' do + it "allows filtering by multiple groups" do + sam = Fabricate(:user, username: "sam") + samantha = Fabricate(:user, username: "samantha") + group_1 = Fabricate(:group) - sam = Fabricate(:user, username: 'sam') - group_2 = Fabricate(:group) - samantha = Fabricate(:user, username: 'samantha') group_1.add(sam) + + group_2 = Fabricate(:group) group_2.add(samantha) results = search_for("sam", groups: [group_1, group_2]) - expect(results.count).to eq(2) + expect(results).to eq [sam, samantha].map(&:username) end context "with seed data" do - fab!(:post1) { Fabricate :post, user: user1, topic: topic } - fab!(:post2) { Fabricate :post, user: user2, topic: topic2 } - fab!(:post3) { Fabricate :post, user: user3, topic: topic } - fab!(:post4) { Fabricate :post, user: user4, topic: topic } - fab!(:post5) { Fabricate :post, user: user5, topic: topic3 } - fab!(:post6) { Fabricate :post, user: user6, topic: topic } + fab!(:post1) { Fabricate :post, user: mr_b, topic: topic } + fab!(:post2) { Fabricate :post, user: mr_blue, topic: topic2 } + fab!(:post3) { Fabricate :post, user: mr_orange, topic: topic } + fab!(:post4) { Fabricate :post, user: mr_pink, topic: topic } + fab!(:post5) { Fabricate :post, user: mr_brown, topic: topic3 } + fab!(:post6) { Fabricate :post, user: mr_white, topic: topic } fab!(:post7) { Fabricate :post, user: staged, topic: topic4 } - before { user6.update(suspended_at: 1.day.ago, suspended_till: 1.year.from_now) } + before { mr_white.update(suspended_at: 1.day.ago, suspended_till: 1.year.from_now) } it "can search by name and username" do # normal search - results = search_for(user1.name.split(" ").first) - expect(results.size).to eq(1) - expect(results.first.username).to eq(user1.username) + results = search_for(mr_b.name.split.first) + expect(results).to eq [mr_b.username] # lower case - results = search_for(user1.name.split(" ").first.downcase) - expect(results.size).to eq(1) - expect(results.first).to eq(user1) + results = search_for(mr_b.name.split.first.downcase) + expect(results).to eq [mr_b.username] # username - results = search_for(user4.username) - expect(results.size).to eq(1) - expect(results.first).to eq(user4) + results = search_for(mr_pink.username) + expect(results).to eq [mr_pink.username] # case insensitive - results = search_for(user4.username.upcase) - expect(results.size).to eq(1) - expect(results.first).to eq(user4) + results = search_for(mr_pink.username.upcase) + expect(results).to eq [mr_pink.username] end it "handles substring search correctly" do - # substrings - # only staff members see suspended users in results results = search_for("mr") - expect(results.size).to eq(5) - expect(results).not_to include(user6) - expect(search_for("mr", searching_user: user1).size).to eq(5) + expect(results).to eq [mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username) + + results = search_for("mr", searching_user: mr_b) + expect(results).to eq [mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username) + + # only staff members see suspended users in results + results = search_for("mr", searching_user: moderator) + expect(results).to eq [mr_white, mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username) results = search_for("mr", searching_user: admin) - expect(results.size).to eq(6) - expect(results).to include(user6) - expect(search_for("mr", searching_user: moderator).size).to eq(6) + expect(results).to eq [mr_white, mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username) - results = search_for(user1.username, searching_user: admin) - expect(results.size).to eq(3) + results = search_for(mr_b.username, searching_user: admin) + expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username) results = search_for("MR", searching_user: admin) - expect(results.size).to eq(6) + expect(results).to eq [mr_white, mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username) results = search_for("MRB", searching_user: admin, limit: 2) - expect(results.size).to eq(2) + expect(results).to eq [mr_b, mr_brown].map(&:username) end it "prioritises topic participants" do - # topic priority - results = search_for(user1.username, topic_id: topic.id) - expect(results.first).to eq(user1) + results = search_for(mr_b.username, topic_id: topic.id) + expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username) - results = search_for(user1.username, topic_id: topic2.id) - expect(results[1]).to eq(user2) + results = search_for(mr_b.username, topic_id: topic2.id) + expect(results).to eq [mr_b, mr_blue, mr_brown].map(&:username) - results = search_for(user1.username, topic_id: topic3.id) - expect(results[1]).to eq(user5) + results = search_for(mr_b.username, topic_id: topic3.id) + expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username) end it "only reveals topic participants to people with permission" do @@ -187,37 +180,46 @@ describe UserSearch do # Random user, does not have access expect do - search_for("", topic_id: pm_topic.id, searching_user: user1) + search_for("", topic_id: pm_topic.id, searching_user: mr_b) end.to raise_error(Discourse::InvalidAccess) - pm_topic.invite(pm_topic.user, user1.username) - results = search_for("", topic_id: pm_topic.id, searching_user: user1) - expect(results.length).to eq(1) - expect(results[0]).to eq(pm_topic.user) + pm_topic.invite(pm_topic.user, mr_b.username) + + results = search_for("", topic_id: pm_topic.id, searching_user: mr_b) + expect(results).to eq [pm_topic.user.username] end it "only searches by name when enabled" do # When searching by name is enabled, it returns the record SiteSetting.enable_names = true results = search_for("Tarantino") - expect(results.size).to eq(1) + expect(results).to eq [mr_brown.username] results = search_for("coding") - expect(results.size).to eq(0) + expect(results).to be_blank results = search_for("z") - expect(results.size).to eq(0) + expect(results).to be_blank # When searching by name is disabled, it will not return the record SiteSetting.enable_names = false results = search_for("Tarantino") - expect(results.size).to eq(0) + expect(results).to be_blank end it "prioritises exact matches" do - # find an exact match first results = search_for("mrB") - expect(results.first.username).to eq(user1.username) + expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username) + end + + it "doesn't prioritises exact matches mentions for users who haven't been seen in over a year" do + abcdef = Fabricate(:user, username: "abcdef", last_seen_at: 2.days.ago) + abcde = Fabricate(:user, username: "abcde", last_seen_at: 2.weeks.ago) + abcd = Fabricate(:user, username: "abcd", last_seen_at: 2.months.ago) + abc = Fabricate(:user, username: "abc", last_seen_at: 2.years.ago) + + results = search_for("abc", topic_id: topic.id) + expect(results).to eq [abcdef, abcde, abcd, abc].map(&:username) end it "does not include self, staged or inactive" do @@ -230,12 +232,11 @@ describe UserSearch do expect(results).to be_blank results = search_for(staged.username, include_staged_users: true) - expect(results.first.username).to eq(staged.username) + expect(results).to eq [staged.username] - results = search_for("", topic_id: topic.id, searching_user: user1) - - # mrb is omitted, mrb is current user - expect(results.map(&:username)).to eq(["mrpink", "mrorange"]) + # mrb is omitted since they're the searching user + results = search_for("", topic_id: topic.id, searching_user: mr_b) + expect(results).to eq [mr_pink, mr_orange].map(&:username) end end end diff --git a/spec/requests/admin/dashboard_controller_spec.rb b/spec/requests/admin/dashboard_controller_spec.rb index 46602ae91d..b838f169dc 100644 --- a/spec/requests/admin/dashboard_controller_spec.rb +++ b/spec/requests/admin/dashboard_controller_spec.rb @@ -15,6 +15,15 @@ describe Admin::DashboardController do context 'while logged in as an admin' do fab!(:admin) { Fabricate(:admin) } + def populate_new_features + sample_features = [ + { "id" => "1", "emoji" => "🤾", "title" => "Cool Beans", "description" => "Now beans are included", "created_at" => Time.zone.now - 40.minutes }, + { "id" => "2", "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Legumes too!", "created_at" => Time.zone.now - 20.minutes } + ] + + Discourse.redis.set('new_features', MultiJson.dump(sample_features)) + end + before do sign_in(admin) end @@ -77,5 +86,49 @@ describe Admin::DashboardController do end end end + + describe '#new_features' do + before do + Discourse.redis.del "new_features_last_seen_user_#{admin.id}" + Discourse.redis.del "new_features" + end + + it 'is empty by default' do + get "/admin/dashboard/new-features.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json['new_features']).to eq(nil) + end + + it 'fails gracefully for invalid JSON' do + Discourse.redis.set("new_features", "INVALID JSON") + get "/admin/dashboard/new-features.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json['new_features']).to eq(nil) + end + + it 'includes new features when available' do + populate_new_features + + get "/admin/dashboard/new-features.json" + expect(response.status).to eq(200) + json = response.parsed_body + + expect(json['new_features'].length).to eq(2) + expect(json['new_features'][0]["emoji"]).to eq("🙈") + expect(json['new_features'][0]["title"]).to eq("Fancy Legumes") + end + end + + describe '#mark_new_features_as_seen' do + it 'resets last seen for a given user' do + populate_new_features + put "/admin/dashboard/mark-new-features-as-seen.json" + + expect(response.status).to eq(200) + expect(DiscourseUpdates.new_features_last_seen(admin.id)).not_to eq(nil) + end + end end end diff --git a/spec/requests/api/schemas/json/tag_group_create_request.json b/spec/requests/api/schemas/json/tag_group_create_request.json new file mode 100644 index 0000000000..2e364e13d9 --- /dev/null +++ b/spec/requests/api/schemas/json/tag_group_create_request.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] +} diff --git a/spec/requests/api/schemas/json/tag_group_create_response.json b/spec/requests/api/schemas/json/tag_group_create_response.json new file mode 100644 index 0000000000..222181c7f2 --- /dev/null +++ b/spec/requests/api/schemas/json/tag_group_create_response.json @@ -0,0 +1,46 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "tag_group": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag_names": { + "type": "array", + "items": [ + + ] + }, + "parent_tag_name": { + "type": "array", + "items": [ + + ] + }, + "one_per_topic": { + "type": "boolean" + }, + "permissions": { + "type": "object" + } + }, + "required": [ + "id", + "name", + "tag_names", + "parent_tag_name", + "one_per_topic", + "permissions" + ] + } + }, + "required": [ + "tag_group" + ] +} diff --git a/spec/requests/api/schemas/json/upload_create_request.json b/spec/requests/api/schemas/json/upload_create_request.json new file mode 100644 index 0000000000..e9349bcba0 --- /dev/null +++ b/spec/requests/api/schemas/json/upload_create_request.json @@ -0,0 +1,30 @@ +{ + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "avatar", + "profile_background", + "card_background", + "custom_emoji", + "composer" + ] + }, + "user_id": { + "type": "integer", + "description": "required if uploading an avatar" + }, + "synchronous": { + "type": "boolean", + "description": "Use this flag to return an id and url" + }, + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "type" + ] +} diff --git a/spec/requests/api/schemas/json/upload_create_response.json b/spec/requests/api/schemas/json/upload_create_response.json new file mode 100644 index 0000000000..05555c7b9e --- /dev/null +++ b/spec/requests/api/schemas/json/upload_create_response.json @@ -0,0 +1,59 @@ +{ + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" + }, + "url": { + "type": "string" + }, + "original_filename": { + "type": "string" + }, + "filesize": { + "type": "integer" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + }, + "thumbnail_width": { + "type": "integer" + }, + "thumbnail_height": { + "type": "integer" + }, + "extension": { + "type": "string" + }, + "short_url": { + "type": "string" + }, + "short_path": { + "type": "string" + }, + "retain_hours": { + "type": ["string", "null"] + }, + "human_filesize": { + "type": "string" + } + }, + "required": [ + "id", + "url", + "original_filename", + "filesize", + "width", + "height", + "thumbnail_width", + "thumbnail_height", + "extension", + "short_url", + "short_path", + "retain_hours", + "human_filesize" + ] +} diff --git a/spec/requests/api/schemas/schema_loader.rb b/spec/requests/api/schemas/schema_loader.rb new file mode 100644 index 0000000000..872e47d74a --- /dev/null +++ b/spec/requests/api/schemas/schema_loader.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'json' + +module SpecSchemas + + class SpecLoader + + def initialize(filename) + @filename = filename + end + + def load + JSON.parse(File.read(File.join(__dir__, "json", "#{@filename}.json"))) + end + end + +end diff --git a/spec/requests/api/shared/shared_examples.rb b/spec/requests/api/shared/shared_examples.rb new file mode 100644 index 0000000000..f6e732dce5 --- /dev/null +++ b/spec/requests/api/shared/shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples "a JSON endpoint" do |expected_response_status| + before do |example| + submit_request(example.metadata) + end + + def expect_schema_valid(schemer, params) + valid = schemer.valid?(params) + unless valid # for debugging + puts + puts "RESPONSE: #{params}" + puts "VALIDATION DETAILS: #{schemer.validate(params).to_a[0]["details"]}" + end + expect(valid).to eq(true) + end + + describe "response status" do + it "returns expected response status" do + expect(response.status).to eq(expected_response_status) + end + end + + describe "request body" do + it "matches the documented request schema" do |example| + schemer = JSONSchemer.schema(expected_request_schema) + expect_schema_valid(schemer, params) + end + end + + describe "response body" do + let(:json_response) { JSON.parse(response.body) } + + it "matches the documented response schema" do |example| + schemer = JSONSchemer.schema( + expected_response_schema, + ) + expect_schema_valid(schemer, json_response) + end + end +end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index b5c53cf4fb..160100865d 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -60,46 +60,22 @@ describe 'tags' do post 'Creates a tag group' do tags 'Tags' consumes 'application/json' - parameter name: :post_body, in: :body, schema: { - type: :object, - properties: { - name: { type: :string } - }, - required: [ 'name' ] - } + expected_request_schema = load_spec_schema('tag_group_create_request') + + parameter name: :params, in: :body, schema: expected_request_schema produces 'application/json' response '200', 'tag group created' do - schema type: :object, properties: { - tag_group: { - type: :object, - properties: { - id: { type: :integer }, - name: { type: :string }, - tag_names: { - type: :array, - items: { - }, - }, - parent_tag_name: { - type: :array, - items: { - }, - }, - one_per_topic: { type: :boolean }, - permissions: { - type: :object, - properties: { - everyone: { type: :integer }, - } - }, - } - }, - } + expected_response_schema = load_spec_schema('tag_group_create_response') - let(:post_body) { { name: 'todo' } } + let(:params) { { 'name' => 'todo' } } - run_test! + schema(expected_response_schema) + + it_behaves_like "a JSON endpoint", 200 do + let(:expected_response_schema) { expected_response_schema } + let(:expected_request_schema) { expected_request_schema } + end end end diff --git a/spec/requests/api/uploads_spec.rb b/spec/requests/api/uploads_spec.rb new file mode 100644 index 0000000000..af21fa91dc --- /dev/null +++ b/spec/requests/api/uploads_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require 'swagger_helper' + +describe 'uploads' do + + let(:admin) { Fabricate(:admin) } + let(:logo_file) { file_from_fixtures("logo.png") } + let(:logo) { Rack::Test::UploadedFile.new(logo_file) } + + before do + Jobs.run_immediately! + sign_in(admin) + end + + path '/uploads.json' do + post 'Creates an upload' do + tags 'Uploads' + consumes 'multipart/form-data' + + expected_request_schema = load_spec_schema('upload_create_request') + parameter name: :params, in: :body, schema: expected_request_schema + + let(:params) { { + type: 'avatar', + user_id: admin.id, + synchronous: true, + file: logo + } } + + produces 'application/json' + response '200', 'file uploaded' do + expected_response_schema = load_spec_schema('upload_create_response') + schema(expected_response_schema) + + # Skipping this test for now until https://github.com/rswag/rswag/issues/348 + # is resolved. This still allows the docs to be generated for this endpoint though. + xit + end + + end + end +end diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 5e82551fc2..1ac69118a7 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -104,11 +104,21 @@ RSpec.describe ApplicationController do end it 'contains authentication data when cookies exist' do - COOKIE_DATA = "someauthenticationdata" - cookies['authentication_data'] = COOKIE_DATA + cookie_data = "someauthenticationdata" + cookies['authentication_data'] = cookie_data get '/login' expect(response.status).to eq(200) - expect(response.body).to include("data-authentication-data=\"#{COOKIE_DATA }\"") + expect(response.body).to include("data-authentication-data=\"#{cookie_data}\"") + expect(response.headers["Set-Cookie"]).to include("authentication_data=;") # Delete cookie + end + + it 'deletes authentication data cookie even if already authenticated' do + sign_in(Fabricate(:user)) + cookies['authentication_data'] = "someauthenticationdata" + get '/' + expect(response.status).to eq(200) + expect(response.body).not_to include("data-authentication-data=") + expect(response.headers["Set-Cookie"]).to include("authentication_data=;") # Delete cookie end end diff --git a/spec/requests/do_not_disturb_controller_spec.rb b/spec/requests/do_not_disturb_controller_spec.rb index 21c6f7a374..1f3f65b3fe 100644 --- a/spec/requests/do_not_disturb_controller_spec.rb +++ b/spec/requests/do_not_disturb_controller_spec.rb @@ -44,13 +44,14 @@ describe DoNotDisturbController do end describe "#destroy" do - it "process notifications that came in during DND" do + it "process shelved notifications that came in during DND" do user.do_not_disturb_timings.create(starts_at: 2.days.ago, ends_at: 2.days.from_now) notification = Notification.create(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1) - expect(notification.processed).to eq(false) + expect(notification.shelved_notification).to be_present delete "/do-not-disturb.json" - expect(notification.reload.processed).to eq(true) + expect { notification.shelved_notification.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(user.do_not_disturb?).to eq(false) end end end diff --git a/spec/requests/invites_controller_spec.rb b/spec/requests/invites_controller_spec.rb index 0648dc17dc..3edce1997d 100644 --- a/spec/requests/invites_controller_spec.rb +++ b/spec/requests/invites_controller_spec.rb @@ -145,11 +145,11 @@ describe InvitesController do expect(Invite.find_by(email: email).invited_groups.count).to eq(1) end - it "allows admin to send multiple invites to same email" do + it "does not allow admins to send multiple invites to same email" do user = sign_in(admin) invite = Invite.invite_by_email("invite@example.com", user) post "/invites.json", params: { email: invite.email } - expect(response.status).to eq(200) + expect(response.status).to eq(422) end it "responds with error message in case of validation failure" do diff --git a/spec/requests/reviewables_controller_spec.rb b/spec/requests/reviewables_controller_spec.rb index 521a5cdefc..708add4968 100644 --- a/spec/requests/reviewables_controller_spec.rb +++ b/spec/requests/reviewables_controller_spec.rb @@ -357,6 +357,7 @@ describe ReviewablesController do it "succeeds for a valid action" do other_reviewable = Fabricate(:reviewable) + SiteSetting.must_approve_users = true put "/review/#{reviewable.id}/perform/approve_user.json?version=#{reviewable.version}" expect(response.code).to eq("200") json = response.parsed_body @@ -369,6 +370,20 @@ describe ReviewablesController do expect(reviewable.reload.version).to eq(1) expect(other_reviewable.reload.version).to eq(0) + + job = Jobs::CriticalUserEmail.jobs.first + expect(job).to be_present + expect(job['args'][0]['type']).to eq('signup_after_approval') + end + + it "doesn't send email when `send_email` is false" do + other_reviewable = Fabricate(:reviewable) + + SiteSetting.must_approve_users = true + put "/review/#{reviewable.id}/perform/approve_user.json?version=#{reviewable.version}&send_email=false" + + job = Jobs::CriticalUserEmail.jobs.first + expect(job).to be_blank end context "claims" do diff --git a/spec/requests/similar_topics_controller_spec.rb b/spec/requests/similar_topics_controller_spec.rb index cef7f5e205..8ecc048668 100644 --- a/spec/requests/similar_topics_controller_spec.rb +++ b/spec/requests/similar_topics_controller_spec.rb @@ -17,7 +17,7 @@ describe SimilarTopicsController do def reindex_posts SearchIndexer.enable - Jobs::ReindexSearch.new.rebuild_problem_posts + Jobs::ReindexSearch.new.rebuild_posts end it "requires a title param" do diff --git a/spec/serializers/group_show_serializer_spec.rb b/spec/serializers/group_show_serializer_spec.rb index a6acacdfdb..00308ebb4c 100644 --- a/spec/serializers/group_show_serializer_spec.rb +++ b/spec/serializers/group_show_serializer_spec.rb @@ -79,6 +79,7 @@ describe GroupShowSerializer do it 'are visible' do expect(subject.as_json[:email_username]).to eq('foo@bar.com') expect(subject.as_json[:email_password]).to eq('pa$$w0rd') + expect(subject.as_json[:message_count]).to eq(0) end end end diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb index 84723680e8..d8ec0597ea 100644 --- a/spec/services/user_anonymizer_spec.rb +++ b/spec/services/user_anonymizer_spec.rb @@ -210,12 +210,23 @@ describe UserAnonymizer do end it "removes api key" do - ApiKey.create(user_id: user.id) + ApiKey.create!(user_id: user.id) + expect { make_anonymous }.to change { ApiKey.count }.by(-1) + user.reload expect(user.api_keys).to be_empty end + it "removes user api key" do + user_api_key = Fabricate(:user_api_key, user: user) + + expect { make_anonymous }.to change { UserApiKey.count }.by(-1) + + user.reload + expect(user.user_api_keys).to be_empty + end + context "executes job" do before do Jobs.run_immediately! diff --git a/spec/services/user_updater_spec.rb b/spec/services/user_updater_spec.rb index abbaadf935..7ed290999e 100644 --- a/spec/services/user_updater_spec.rb +++ b/spec/services/user_updater_spec.rb @@ -251,6 +251,18 @@ describe UserUpdater do updater.update(user_notification_schedule: schedule_attrs) }.to change { user.do_not_disturb_timings.count }.by(4) end + + it "removes do_not_disturb_timings when the schedule is disabled" do + updater = UserUpdater.new(acting_user, user) + updater.update(user_notification_schedule: schedule_attrs) + expect(user.user_notification_schedule.enabled).to eq(true) + + schedule_attrs[:enabled] = false + updater.update(user_notification_schedule: schedule_attrs) + + expect(user.user_notification_schedule.enabled).to eq(false) + expect(user.do_not_disturb_timings.count).to eq(0) + end end context 'when sso overrides bio' do diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 12d9f54718..262c28f86c 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -1,6 +1,17 @@ # frozen_string_literal: true require 'rails_helper' +require 'json_schemer' + +# Require schema files +Dir["./spec/requests/api/schemas/*.rb"].each { |file| require file } + +# Require shared spec examples +Dir["./spec/requests/api/shared/*.rb"].each { |file| require file } + +def load_spec_schema(name) + SpecSchemas::SpecLoader.new(name).load +end RSpec.configure do |config| # Specify a root folder where Swagger JSON files are generated diff --git a/test/run-qunit.js b/test/run-qunit.js index 018b14b31b..9438b34679 100644 --- a/test/run-qunit.js +++ b/test/run-qunit.js @@ -5,7 +5,7 @@ // Requires chrome-launcher and chrome-remote-interface from npm // An up-to-date version of chrome is also required -var args = process.argv.slice(2); +let args = process.argv.slice(2); if (args.length < 1 || args.length > 3) { console.log("Usage: node run-qunit.js "); @@ -128,9 +128,9 @@ async function runAllTests() { } const timeout = parseInt(args[1] || 300000, 10); - var start = Date.now(); + let start = Date.now(); - var interval; + let interval; let runTests = async function () { if (Date.now() > start + timeout) { @@ -218,7 +218,7 @@ function logQUnit() { return; } - var msg = "\n Assertion Failed:"; + let msg = "\n Assertion Failed:"; if (context.message) { msg += " " + context.message; } @@ -256,7 +256,7 @@ function logQUnit() { console.log("\n"); } - var stats = [ + let stats = [ "Time: " + context.runtime + "ms", "Total: " + context.total, "Passed: " + context.passed, diff --git a/yarn.lock b/yarn.lock index 4156ac0861..971a5fb4b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -147,6 +147,19 @@ "@glimmer/interfaces" "^0.54.2" "@simple-dom/interface" "^1.4.0" +"@mixer/parallel-prettier@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@mixer/parallel-prettier/-/parallel-prettier-2.0.1.tgz#fd69bb55e38b3c1dbb2f1a534ea1a0cd3fe34946" + integrity sha512-bx/rdOhJ2EOxQTm4cQQBsdbg+IwaQHh/ugIZMsWNEILXLJWQukCv+6fDUBZkoIk/zEoW45uIrCugARCuuDwrWw== + dependencies: + chalk "^4.1.0" + commander "^7.0.0" + glob-stream "^6.1.0" + ignore "^5.1.8" + ora "^5.3.0" + prettier "^2.0.4" + rxjs "^6.6.3" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -394,6 +407,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -407,6 +425,15 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +bl@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + blueimp-canvas-to-blob@3: version "3.28.0" resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.28.0.tgz#c8ab4dc6bb08774a7f273798cdf94b0776adf6c8" @@ -489,6 +516,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -633,6 +668,11 @@ cli-spinners@^2.2.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== +cli-spinners@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" + integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -720,6 +760,11 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.0.0.tgz#3e2bbfd8bb6724760980988fb5b22b7ee6b71ab2" + integrity sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -850,6 +895,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + ember-rfc176-data@^0.3.11: version "0.3.15" resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.15.tgz#af3f1da5a0339b6feda380edc2f7190e0f416c2d" @@ -901,7 +956,7 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -935,10 +990,10 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -eslint-config-discourse@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/eslint-config-discourse/-/eslint-config-discourse-1.1.5.tgz#29e274eeccd3071d86aaa9333c6550ceeb23b1cd" - integrity sha512-/2DgKfIAKRHP+ktNvYCvK2z0NVmgpMe4le1HcrXtj0EzRswqsUN1kbaU81X3cnG4iEuKxyf0n0Hp+2kHuneatg== +eslint-config-discourse@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/eslint-config-discourse/-/eslint-config-discourse-1.1.8.tgz#c297831876811ea08203aa348d1ba2a963b2ae78" + integrity sha512-ZSQfhliiO5Cfa7WcKoMkN4wW/1rBJpecpMJpfjiFsElfgPj4EV4Pzksi5CvFnRbJDoZh6DYYrQfO+tW062VOUA== dependencies: babel-eslint "^10.1.0" ember-template-lint "^2.11.0" @@ -948,7 +1003,7 @@ eslint-config-discourse@^1.1.5: eslint-plugin-ember "^6.10.0" eslint-plugin-lodash "^7.1.0" eslint-plugin-node "^8.0.0" - prettier "2.1.2" + prettier "2.2.1" eslint-plugin-discourse-ember@latest: version "0.0.3" @@ -1155,6 +1210,11 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -1356,6 +1416,14 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + glob-parent@^5.0.0, glob-parent@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" @@ -1363,6 +1431,22 @@ glob-parent@^5.0.0, glob-parent@^5.1.0: dependencies: is-glob "^4.0.1" +glob-stream@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" + integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= + dependencies: + extend "^3.0.0" + glob "^7.1.1" + glob-parent "^3.1.0" + is-negated-glob "^1.0.0" + ordered-read-streams "^1.0.0" + pumpify "^1.3.5" + readable-stream "^2.1.5" + remove-trailing-separator "^1.0.1" + to-absolute-glob "^2.0.0" + unique-stream "^2.0.2" + glob@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" @@ -1375,7 +1459,7 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3: +glob@^7.1.1, glob@^7.1.3: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -1515,12 +1599,17 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.2, ignore@^5.1.4: +ignore@^5.0.2, ignore@^5.1.4, ignore@^5.1.8: version "5.1.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== @@ -1546,7 +1635,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1580,6 +1669,14 @@ intersection-observer@^0.5.1: resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.5.1.tgz#e340fc56ce74290fe2b2394d1ce88c4353ac6dfa" integrity sha512-Zd7Plneq82kiXFixs7bX62YnuZ0BMRci9br7io88LwDyF3V43cQMI+G5IiTlTNTt+LsDUppl19J/M2Fp9UkH6g== +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -1682,6 +1779,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -1701,11 +1803,25 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + is-valid-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" @@ -1905,6 +2021,13 @@ log-symbols@^3.0.0: dependencies: chalk "^2.4.2" +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -2220,6 +2343,27 @@ ora@^4.0.4: strip-ansi "^6.0.0" wcwidth "^1.0.1" +ora@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" + integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g== + dependencies: + bl "^4.0.3" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + log-symbols "^4.0.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +ordered-read-streams@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" + integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= + dependencies: + readable-stream "^2.0.1" + os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -2280,6 +2424,11 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -2345,10 +2494,15 @@ pretender@^1.6: fake-xml-http-request "^1.6.0" route-recognizer "^0.3.3" -prettier@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" - integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== +prettier@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + +prettier@^2.0.4: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== process-nextick-args@~2.0.0: version "2.0.1" @@ -2365,6 +2519,14 @@ proxy-from-env@^1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -2373,6 +2535,15 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +pumpify@^1.3.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -2405,7 +2576,7 @@ qunit@2.8.0: sane "^4.0.0" walk-sync "0.3.2" -readable-stream@^2.2.2: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -2418,6 +2589,15 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -2564,7 +2744,7 @@ run-parallel@^1.1.9: resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== -rxjs@^6.6.0: +rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== @@ -2576,6 +2756,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -2770,6 +2955,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + string-width@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -2788,6 +2978,13 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2848,6 +3045,22 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +through2-filter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" + integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + +through2@~2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -2872,6 +3085,14 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= +to-absolute-glob@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" + integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs= + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + 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" @@ -2971,6 +3192,11 @@ ultron@~1.1.0: resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -2981,6 +3207,14 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +unique-stream@^2.0.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" + integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== + dependencies: + json-stable-stringify-without-jsonify "^1.0.1" + through2-filter "^3.0.0" + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -3006,7 +3240,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -3143,6 +3377,11 @@ ws@^6.1.0: dependencies: async-limiter "~1.0.0" +xtend@~4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"