diff --git a/.github/workflows/ember.yml b/.github/workflows/ember.yml
index 538b16b316..b906767d01 100644
--- a/.github/workflows/ember.yml
+++ b/.github/workflows/ember.yml
@@ -1,4 +1,4 @@
-name: Ember CLI tests
+name: (experimental) Ember CLI tests (core)
on:
pull_request:
@@ -9,10 +9,14 @@ on:
jobs:
build:
name: run
- if: true
runs-on: ubuntu-latest
container: discourse/discourse_test:release
- timeout-minutes: 40
+ timeout-minutes: 60
+
+ strategy:
+ fail-fast: false
+ matrix:
+ browser: ["Chrome", "Firefox", "Headless Firefox"]
steps:
- uses: actions/checkout@master
@@ -43,5 +47,5 @@ jobs:
- name: Core QUnit
working-directory: ./app/assets/javascripts/discourse
- run: yarn ember test
- timeout-minutes: 30
+ run: sudo -E -u discourse -H yarn ember test --launch "${{ matrix.browser }}"
+ timeout-minutes: 60
diff --git a/.github/workflows/ember_with_plugins.yml b/.github/workflows/ember_with_plugins.yml
new file mode 100644
index 0000000000..ec355c0143
--- /dev/null
+++ b/.github/workflows/ember_with_plugins.yml
@@ -0,0 +1,49 @@
+name: (experimental) Ember CLI tests (plugins)
+
+on:
+ schedule:
+ - cron: "0 0 * * *"
+
+jobs:
+ build:
+ name: run
+ runs-on: ubuntu-latest
+ container: discourse/discourse_test:release
+ timeout-minutes: 60
+
+ steps:
+ - uses: actions/checkout@master
+ with:
+ fetch-depth: 1
+
+ - name: Setup Git
+ run: |
+ git config --global user.email "ci@ci.invalid"
+ git config --global user.name "Discourse CI"
+
+ - name: Get yarn cache directory
+ id: yarn-cache-dir
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - name: Yarn cache
+ uses: actions/cache@v2
+ id: yarn-cache
+ with:
+ path: ${{ steps.yarn-cache-dir.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Official Plugins Install
+ run: |
+ bundle config --local path vendor/bundle
+ bundle config --local deployment true
+ bundle config --local without development
+ bundle install --jobs 4
+ bundle clean
+ bundle exec rake plugin:install_all_official
+
+ - name: QUnit
+ working-directory: ./app/assets/javascripts/discourse
+ run: QUNIT_EMBER_CLI=1 sudo -E -u discourse -H rake plugin:qunit
+ timeout-minutes: 60
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1f47223bdf..21234f82ad 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -28,7 +28,6 @@ jobs:
build_type: [backend, frontend, annotations]
target: [core, plugins]
postgres: ["13"]
- redis: ["6.x"]
exclude:
- build_type: annotations
target: plugins
@@ -59,10 +58,9 @@ jobs:
git config --global user.email "ci@ci.invalid"
git config --global user.name "Discourse CI"
- - name: Setup redis
- uses: shogo82148/actions-setup-redis@v1
- with:
- redis-version: ${{ matrix.redis }}
+ - name: Start redis
+ run: |
+ redis-server /etc/redis/redis.conf &
- name: Bundler cache
uses: actions/cache@v2
diff --git a/Gemfile b/Gemfile
index 9c2ff7cdbb..e09839e88a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -210,6 +210,9 @@ gem 'gc_tracer', require: false, platform: :mri
# required for feed importing and embedding
gem 'ruby-readability', require: false
+# rss gem is a bundled gem from Ruby 3 onwards
+gem 'rss', require: false
+
gem 'stackprof', require: false, platform: :mri
gem 'memory_profiler', require: false, platform: :mri
diff --git a/Gemfile.lock b/Gemfile.lock
index 152aabb99e..0e75936277 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -53,8 +53,8 @@ GEM
rake (>= 10.4, < 14.0)
ast (2.4.2)
aws-eventstream (1.2.0)
- aws-partitions (1.432.0)
- aws-sdk-core (3.112.1)
+ aws-partitions (1.516.0)
+ aws-sdk-core (3.121.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
@@ -66,10 +66,10 @@ GEM
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
- aws-sdk-sns (1.38.0)
- aws-sdk-core (~> 3, >= 3.112.0)
+ aws-sdk-sns (1.46.0)
+ aws-sdk-core (~> 3, >= 3.121.2)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.2.3)
+ aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
barber (0.12.2)
ember-source (>= 1.0, < 3.1)
@@ -80,7 +80,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
- bootsnap (1.8.1)
+ bootsnap (1.9.1)
msgpack (~> 1.0)
builder (3.2.4)
bullet (6.1.5)
@@ -129,14 +129,14 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.10.0)
- excon (0.85.0)
+ excon (0.87.0)
execjs (2.8.1)
exifr (1.3.9)
fabrication (2.22.0)
faker (2.19.0)
i18n (>= 1.6, < 2)
fakeweb (1.3.0)
- faraday (1.7.1)
+ faraday (1.8.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -173,7 +173,7 @@ GEM
http_accept_language (2.1.1)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
- image_optim (0.30.0)
+ image_optim (0.31.0)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (>= 1.5, < 3)
@@ -181,12 +181,13 @@ GEM
progress (~> 3.0, >= 3.0.1)
image_size (2.1.2)
in_threads (1.5.4)
+ ipaddr (1.2.2)
jmespath (1.4.0)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
- json (2.5.1)
+ json (2.6.0)
json-schema (2.8.1)
addressable (>= 2.4)
json_schemer (0.2.18)
@@ -194,7 +195,7 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
- jwt (2.2.3)
+ jwt (2.3.0)
kgio (2.11.4)
libv8-node (15.14.0.1)
libv8-node (15.14.0.1-arm64-darwin-20)
@@ -225,7 +226,7 @@ GEM
message_bus (3.3.6)
rack (>= 1.1.3)
method_source (1.0.0)
- mini_mime (1.1.1)
+ mini_mime (1.1.2)
mini_portile2 (2.6.1)
mini_racer (0.4.0)
libv8-node (~> 15.14.0.0)
@@ -244,14 +245,14 @@ GEM
multipart-post (2.1.1)
mustache (1.1.1)
nio4r (2.5.8)
- nokogiri (1.12.4)
+ nokogiri (1.12.5)
mini_portile2 (~> 2.6.1)
racc (~> 1.4)
- nokogiri (1.12.4-arm64-darwin)
+ nokogiri (1.12.5-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.12.4-x86_64-darwin)
+ nokogiri (1.12.5-x86_64-darwin)
racc (~> 1.4)
- nokogiri (1.12.4-x86_64-linux)
+ nokogiri (1.12.5-x86_64-linux)
racc (~> 1.4)
oauth (0.5.6)
oauth2 (1.4.7)
@@ -283,12 +284,13 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
- openssl (2.2.0)
+ openssl (2.2.1)
+ ipaddr
openssl-signature_algorithm (1.1.1)
openssl (~> 2.0)
optimist (3.0.1)
- parallel (1.20.1)
- parallel_tests (3.7.1)
+ parallel (1.21.0)
+ parallel_tests (3.7.3)
parallel
parser (3.0.2.0)
ast (~> 2.4.1)
@@ -303,10 +305,10 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
- puma (5.4.0)
+ puma (5.5.2)
nio4r (~> 2.0)
r2 (0.2.7)
- racc (1.5.2)
+ racc (1.6.0)
rack (2.2.3)
rack-mini-profiler (2.3.3)
rack (>= 1.2.0)
@@ -323,7 +325,7 @@ GEM
activerecord (~> 6.0)
concurrent-ruby
railties (~> 6.0)
- rails_multisite (3.0.0)
+ rails_multisite (3.1.0)
activerecord (> 5.0, < 7)
railties (> 5.0, < 7)
railties (6.1.4.1)
@@ -343,7 +345,7 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
- redis (4.4.0)
+ redis (4.5.1)
redis-namespace (1.8.1)
redis (>= 3.0.4)
regexp_parser (2.1.1)
@@ -380,28 +382,29 @@ GEM
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.2)
+ rss (0.2.9)
+ rexml
rswag-specs (2.4.0)
activesupport (>= 3.1, < 7.0)
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
- rubocop (1.20.0)
+ rubocop (1.22.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
- rubocop-ast (>= 1.9.1, < 2.0)
+ rubocop-ast (>= 1.12.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.11.0)
+ rubocop-ast (1.12.0)
parser (>= 3.0.1.1)
rubocop-discourse (2.4.2)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
- rubocop-rspec (2.4.0)
- rubocop (~> 1.0)
- rubocop-ast (>= 1.1.0)
+ rubocop-rspec (2.5.0)
+ rubocop (~> 1.19)
ruby-prof (1.4.3)
ruby-progressbar (1.11.0)
ruby-readability (0.7.0)
@@ -454,8 +457,8 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.7)
- unicode-display_width (2.0.0)
+ unf_ext (0.0.8)
+ unicode-display_width (2.1.0)
unicorn (6.0.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -581,6 +584,7 @@ DEPENDENCIES
rspec
rspec-html-matchers
rspec-rails
+ rss
rswag-specs
rtlit
rubocop-discourse
diff --git a/README.md b/README.md
index 1410ab5057..cdd241f27d 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,6 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
-
diff --git a/app/assets/javascripts/admin/addon/components/admin-report-chart.js b/app/assets/javascripts/admin/addon/components/admin-report-chart.js
index 7711ca8d3d..1c4ad1caa3 100644
--- a/app/assets/javascripts/admin/addon/components/admin-report-chart.js
+++ b/app/assets/javascripts/admin/addon/components/admin-report-chart.js
@@ -5,6 +5,7 @@ import loadScript from "discourse/lib/load-script";
import { makeArray } from "discourse-common/lib/helpers";
import { number } from "discourse/lib/formatter";
import { schedule } from "@ember/runloop";
+import { bind } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["admin-report-chart"],
@@ -12,23 +13,16 @@ export default Component.extend({
total: 0,
options: null,
- init() {
- this._super(...arguments);
-
- this.resizeHandler = () =>
- discourseDebounce(this, this._scheduleChartRendering, 500);
- },
-
didInsertElement() {
this._super(...arguments);
- $(window).on("resize.chart", this.resizeHandler);
+ window.addEventListener("resize", this._resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
- $(window).off("resize.chart", this.resizeHandler);
+ window.removeEventListener("resize", this._resizeHandler);
this._resetChart();
},
@@ -179,4 +173,9 @@ export default Component.extend({
_applyChartGrouping(model, data, options) {
return Report.collapse(model, data, options.chartGrouping);
},
+
+ @bind
+ _resizeHandler() {
+ discourseDebounce(this, this._scheduleChartRendering, 500);
+ },
});
diff --git a/app/assets/javascripts/admin/addon/components/embeddable-host.js b/app/assets/javascripts/admin/addon/components/embeddable-host.js
index a0f546227b..328ad06e1e 100644
--- a/app/assets/javascripts/admin/addon/components/embeddable-host.js
+++ b/app/assets/javascripts/admin/addon/components/embeddable-host.js
@@ -12,9 +12,20 @@ export default Component.extend(bufferedProperty("host"), {
editToggled: false,
tagName: "tr",
categoryId: null,
+ category: null,
editing: or("host.isNew", "editToggled"),
+ init() {
+ this._super(...arguments);
+
+ const host = this.host;
+ const categoryId = host.category_id || this.site.uncategorized_category_id;
+ const category = Category.findById(categoryId);
+
+ host.set("category", category);
+ },
+
@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || isEmpty(host);
diff --git a/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js b/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js
index 5636c302db..95ed9edfe8 100644
--- a/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js
+++ b/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js
@@ -17,7 +17,7 @@ import { schedule } from "@ember/runloop";
**/
export default Component.extend({
- classNames: ["screened-ip-address-form"],
+ classNames: ["screened-ip-address-form", "inline-form"],
formSubmitted: false,
actionName: "block",
diff --git a/app/assets/javascripts/admin/addon/components/site-settings-image-uploader.js b/app/assets/javascripts/admin/addon/components/site-settings-image-uploader.js
deleted file mode 100644
index 3e19fa8f03..0000000000
--- a/app/assets/javascripts/admin/addon/components/site-settings-image-uploader.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import ImageUploader from "discourse/components/image-uploader";
-
-export default ImageUploader.extend({
- layoutName: "components/image-uploader",
- uploadUrlParams: "&for_site_setting=true",
-});
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-api-keys-new.js b/app/assets/javascripts/admin/addon/controllers/admin-api-keys-new.js
index c90316369a..961c1a5c4b 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-api-keys-new.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-api-keys-new.js
@@ -3,70 +3,83 @@ import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isBlank } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import { get } from "@ember/object";
+import { action, get } from "@ember/object";
+import { equal } from "@ember/object/computed";
import showModal from "discourse/lib/show-modal";
+import { ajax } from "discourse/lib/ajax";
export default Controller.extend({
- userModes: [
- { id: "all", name: I18n.t("admin.api.all_users") },
- { id: "single", name: I18n.t("admin.api.single_user") },
- ],
+ userModes: null,
useGlobalKey: false,
scopes: null,
- @discourseComputed("userMode")
- showUserSelector(mode) {
- return mode === "single";
+ init() {
+ this._super(...arguments);
+
+ this.set("userModes", [
+ { id: "all", name: I18n.t("admin.api.all_users") },
+ { id: "single", name: I18n.t("admin.api.single_user") },
+ ]);
+ this._loadScopes();
},
- @discourseComputed("model.description", "model.username", "userMode")
- saveDisabled(description, username, userMode) {
- if (isBlank(description)) {
+ showUserSelector: equal("userMode", "single"),
+
+ @discourseComputed("model.{description,username}", "showUserSelector")
+ saveDisabled(model, showUserSelector) {
+ if (isBlank(model.description)) {
return true;
}
- if (userMode === "single" && isBlank(username)) {
+ if (showUserSelector && isBlank(model.username)) {
return true;
}
return false;
},
- actions: {
- updateUsername(selected) {
- this.set("model.username", get(selected, "firstObject"));
- },
+ @action
+ updateUsername(selected) {
+ this.set("model.username", get(selected, "firstObject"));
+ },
- changeUserMode(value) {
- if (value === "all") {
- this.model.set("username", null);
- }
- this.set("userMode", value);
- },
+ @action
+ changeUserMode(userMode) {
+ if (userMode === "all") {
+ this.model.set("username", null);
+ }
+ this.set("userMode", userMode);
+ },
- save() {
- if (!this.useGlobalKey) {
- const selectedScopes = Object.values(this.scopes)
- .flat()
- .filter((action) => {
- return action.selected;
- });
+ @action
+ save() {
+ if (!this.useGlobalKey) {
+ const selectedScopes = Object.values(this.scopes)
+ .flat()
+ .filterBy("selected");
- this.model.set("scopes", selectedScopes);
- }
+ this.model.set("scopes", selectedScopes);
+ }
- this.model.save().catch(popupAjaxError);
- },
+ return this.model.save().catch(popupAjaxError);
+ },
- continue() {
- this.transitionToRoute("adminApiKeys.show", this.model.id);
- },
+ @action
+ continue() {
+ this.transitionToRoute("adminApiKeys.show", this.model.id);
+ },
- showURLs(urls) {
- return showModal("admin-api-key-urls", {
- admin: true,
- model: {
- urls,
- },
- });
- },
+ @action
+ showURLs(urls) {
+ return showModal("admin-api-key-urls", {
+ admin: true,
+ model: { urls },
+ });
+ },
+
+ _loadScopes() {
+ return ajax("/admin/api/keys/scopes.json")
+ .then((data) => {
+ this.set("scopes", data.scopes);
+ })
+ .catch(popupAjaxError);
},
});
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show.js b/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show.js
index 000e1d3fc8..959151d96e 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show.js
@@ -146,7 +146,9 @@ export default Controller.extend({
@discourseComputed("model.translations")
translations(translations) {
- return translations.map((setting) => ThemeSettings.create(setting));
+ return translations.map((setting) =>
+ ThemeSettings.create({ ...setting, textarea: true })
+ );
},
hasTranslations: notEmpty("translations"),
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 93ed9c306f..22345e8b82 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
@@ -218,8 +218,15 @@ export default Controller.extend(CanCheckEmails, {
grantAdmin() {
return this.model
.grantAdmin()
- .then(() => {
- bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
+ .then((result) => {
+ if (result.email_confirmation_required) {
+ bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
+ } else {
+ const controller = showModal("grant-admin-second-factor", {
+ model: this.model,
+ });
+ controller.setResult(result);
+ }
})
.catch(popupAjaxError);
},
diff --git a/app/assets/javascripts/admin/addon/mixins/setting-component.js b/app/assets/javascripts/admin/addon/mixins/setting-component.js
index b55c045593..9585168210 100644
--- a/app/assets/javascripts/admin/addon/mixins/setting-component.js
+++ b/app/assets/javascripts/admin/addon/mixins/setting-component.js
@@ -1,14 +1,18 @@
+import { isNone } from "@ember/utils";
+import { fmt, propertyNotEqual } from "discourse/lib/computed";
import { alias, oneWay } from "@ember/object/computed";
import I18n from "I18n";
import Mixin from "@ember/object/mixin";
import { Promise } from "rsvp";
import { ajax } from "discourse/lib/ajax";
import { categoryLinkHTML } from "discourse/helpers/category-link";
-import discourseComputed from "discourse-common/utils/decorators";
+import discourseComputed, { bind } from "discourse-common/utils/decorators";
import { htmlSafe } from "@ember/template";
import { on } from "@ember/object/evented";
import showModal from "discourse/lib/show-modal";
import { warn } from "@ember/debug";
+import { action } from "@ember/object";
+import { splitString } from "discourse/lib/utilities";
const CUSTOM_TYPES = [
"bool",
@@ -32,26 +36,20 @@ const CUSTOM_TYPES = [
const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"];
-function splitPipes(str) {
- if (typeof str === "string") {
- return str.split("|").filter(Boolean);
- } else {
- return [];
- }
-}
-
export default Mixin.create({
classNameBindings: [":row", ":setting", "overridden", "typeClass"],
content: alias("setting"),
validationMessage: null,
isSecret: oneWay("setting.secret"),
+ setting: null,
@discourseComputed("buffered.value", "setting.value")
dirty(bufferVal, settingVal) {
- if (bufferVal === null || bufferVal === undefined) {
+ if (isNone(bufferVal)) {
bufferVal = "";
}
- if (settingVal === null || settingVal === undefined) {
+
+ if (isNone(settingVal)) {
settingVal = "";
}
@@ -61,21 +59,17 @@ export default Mixin.create({
@discourseComputed("setting", "buffered.value")
preview(setting, value) {
// A bit hacky, but allows us to use helpers
- if (setting.get("setting") === "category_style") {
- let category = this.site.get("categories.firstObject");
+ if (setting.setting === "category_style") {
+ const category = this.site.get("categories.firstObject");
if (category) {
- return categoryLinkHTML(category, {
- categoryStyle: value,
- });
+ return categoryLinkHTML(category, { categoryStyle: value });
}
}
- let preview = setting.get("preview");
+
+ const preview = setting.preview;
if (preview) {
- return htmlSafe(
- "