diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4eb8ad5f07..be7d476572 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -25,3 +25,6 @@ c4644c61d97c823b7dd940ffaf0967a104f4b58c # Migrate to app directory 7a2e8d3ead63c7d99e1069fc7823e933f931ba85 + +# DEV: Fix indentation for routes.rb +985900818ff985b04def6aa4c5d99c1aa6dbd45c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5a33d7a45..03f4e109c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - master pull_request: branches-ignore: - - 'tests-passed' + - "tests-passed" jobs: build: @@ -28,12 +28,12 @@ jobs: fail-fast: false matrix: - build_types: [ 'BACKEND', 'FRONTEND', 'LINT' ] - target: [ 'PLUGINS', 'CORE' ] - os: [ ubuntu-latest ] - ruby: [ '2.6' ] - postgres: [ '10' ] - redis: [ '4.x' ] + build_types: ["BACKEND", "FRONTEND", "LINT"] + target: ["PLUGINS", "CORE"] + os: [ubuntu-latest] + ruby: ["2.6"] + postgres: ["10"] + redis: ["4.x"] services: postgres: @@ -77,7 +77,6 @@ jobs: uses: actions/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - architecture: 'x64' - name: Setup bundler run: | @@ -145,6 +144,14 @@ jobs: yarn prettier -v yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.js" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6" + - name: Core English locale + if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE' + run: bundle exec ruby script/i18n_lint.rb "config/**/locales/{client,server}.en.yml" + + - name: Plugin English locale + if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS' + run: bundle exec ruby script/i18n_lint.rb "plugins/**/locales/{client,server}.en.yml" + - name: Core RSpec if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE' run: | @@ -167,5 +174,5 @@ jobs: - name: Plugin QUnit # Tests core plugins in TARGET=CORE, and all plugins in TARGET=PLUGINS if: env.BUILD_TYPE == 'FRONTEND' - run: bundle exec rake plugin:qunit + run: bundle exec rake plugin:qunit['*','1200000'] timeout-minutes: 30 diff --git a/Gemfile b/Gemfile index da983fa8c7..b708152130 100644 --- a/Gemfile +++ b/Gemfile @@ -112,6 +112,7 @@ gem 'oj' gem 'pg' gem 'mini_sql' gem 'pry-rails', require: false +gem 'pry-byebug', require: false gem 'r2', require: false gem 'rake' diff --git a/Gemfile.lock b/Gemfile.lock index 6bf999c7a3..2870d86e7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,9 @@ GIT remote: https://github.com/discourse/rails_failover - revision: 0e668eba86591c20aa7a43f47b0cd36d4eacaeb6 + revision: 66602aa73785851b81c506f0023d3c2a2e40de0a specs: rails_failover (0.4.0) activerecord (~> 6.0) - listen (~> 3.2) railties (~> 6.0) GEM @@ -94,7 +93,7 @@ GEM coderay (1.1.3) colored2 (3.1.2) concurrent-ruby (1.1.6) - connection_pool (2.2.2) + connection_pool (2.2.3) cose (1.0.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 0.4.0) @@ -115,7 +114,7 @@ GEM in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) docile (1.3.2) - email_reply_trimmer (0.1.12) + email_reply_trimmer (0.1.13) ember-data-source (3.0.2) ember-source (>= 2, < 3.0) ember-handlebars-template (0.8.0) @@ -156,7 +155,7 @@ GEM hkdf (0.3.0) htmlentities (4.3.4) http_accept_language (2.1.1) - i18n (1.8.2) + i18n (1.8.3) concurrent-ruby (~> 1.0) image_size (1.5.0) in_threads (1.5.4) @@ -192,7 +191,7 @@ GEM mini_mime (>= 0.1.1) maxminddb (0.1.22) memory_profiler (0.9.14) - message_bus (3.3.0) + message_bus (3.3.1) rack (>= 1.1.3) method_source (1.0.0) mini_mime (1.0.2) @@ -268,6 +267,9 @@ GEM pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.5) @@ -286,7 +288,7 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - rails_multisite (2.1.2) + rails_multisite (2.3.0) activerecord (> 5.0, < 7) railties (> 5.0, < 7) railties (6.0.3.1) @@ -311,6 +313,7 @@ GEM redis (4.1.4) redis-namespace (1.7.0) redis (>= 3.0.4) + regexp_parser (1.7.1) request_store (1.5.0) rack (>= 1.4) rexml (3.2.4) @@ -350,10 +353,11 @@ GEM json-schema (~> 2.2) railties (>= 3.1, < 7.0) rtlit (0.0.5) - rubocop (0.84.0) + rubocop (0.85.1) parallel (~> 1.10) parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.7) rexml rubocop-ast (>= 0.0.3) ruby-progressbar (~> 1.7) @@ -515,6 +519,7 @@ DEPENDENCIES onebox parallel_tests pg + pry-byebug pry-rails puma r2 diff --git a/app/assets/javascripts/admin/components/admin-report.js b/app/assets/javascripts/admin/components/admin-report.js index b9ce9a6fdf..e64e02f3eb 100644 --- a/app/assets/javascripts/admin/components/admin-report.js +++ b/app/assets/javascripts/admin/components/admin-report.js @@ -9,8 +9,8 @@ import ReportLoader from "discourse/lib/reports-loader"; import { exportEntity } from "discourse/lib/export-csv"; import { outputExportResult } from "discourse/lib/export-result"; import Report, { SCHEMA_VERSION } from "admin/models/report"; -import ENV from "discourse-common/config/environment"; import { isPresent } from "@ember/utils"; +import { isTesting } from "discourse-common/config/environment"; const TABLE_OPTIONS = { perPage: 8, @@ -167,8 +167,8 @@ export default Component.extend({ let reportKey = "reports:"; reportKey += [ dataSourceName, - ENV.environment === "test" ? "start" : startDate.replace(/-/g, ""), - ENV.environment === "test" ? "end" : endDate.replace(/-/g, ""), + isTesting() ? "start" : startDate.replace(/-/g, ""), + isTesting() ? "end" : endDate.replace(/-/g, ""), "[:prev_period]", this.get("reportOptions.table.limit"), // Convert all filter values to strings to ensure unique serialization @@ -226,14 +226,18 @@ export default Component.extend({ @action exportCsv() { - const customFilters = this.get("filters.customFilters") || {}; - exportEntity("report", { + const args = { name: this.get("model.type"), start_date: this.startDate.toISOString(true).split("T")[0], - end_date: this.endDate.toISOString(true).split("T")[0], - category_id: customFilters.category, - group_id: customFilters.group - }).then(outputExportResult); + end_date: this.endDate.toISOString(true).split("T")[0] + }; + + const customFilters = this.get("filters.customFilters"); + if (customFilters) { + Object.assign(args, customFilters); + } + + exportEntity("report", args).then(outputExportResult); }, @action diff --git a/app/assets/javascripts/admin/components/resumable-upload.js b/app/assets/javascripts/admin/components/resumable-upload.js index ed95ea68be..97b6f19875 100644 --- a/app/assets/javascripts/admin/components/resumable-upload.js +++ b/app/assets/javascripts/admin/components/resumable-upload.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { later, schedule } from "@ember/runloop"; import Component from "@ember/component"; @@ -31,7 +32,7 @@ export default Component.extend({ @on("init") _initialize() { this.resumable = new Resumable({ - target: Discourse.getURL(this.target), + target: getURL(this.target), maxFiles: 1, // only 1 file at a time headers: { "X-CSRF-Token": document.querySelector("meta[name='csrf-token']") diff --git a/app/assets/javascripts/admin/components/simple-list.js b/app/assets/javascripts/admin/components/simple-list.js new file mode 100644 index 0000000000..3f43885d77 --- /dev/null +++ b/app/assets/javascripts/admin/components/simple-list.js @@ -0,0 +1,57 @@ +import { empty } from "@ember/object/computed"; +import Component from "@ember/component"; +import { action } from "@ember/object"; +import { on } from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNameBindings: [":simple-list", ":value-list"], + inputEmpty: empty("newValue"), + inputDelimiter: null, + newValue: "", + collection: null, + values: null, + + @on("didReceiveAttrs") + _setupCollection() { + this.set("collection", this._splitValues(this.values, this.inputDelimiter)); + }, + + keyDown(event) { + if (event.which === 13) { + this.addValue(this.newValue); + return; + } + }, + + @action + changeValue(index, newValue) { + this.collection.replace(index, 1, [newValue]); + this.collection.arrayContentDidChange(index); + this._onChange(); + }, + + @action + addValue(newValue) { + if (this.inputEmpty) return; + + this.set("newValue", null); + this.collection.addObject(newValue); + this._onChange(); + }, + + @action + removeValue(value) { + this.collection.removeObject(value); + this._onChange(); + }, + + _onChange() { + this.attrs.onChange && this.attrs.onChange(this.collection); + }, + + _splitValues(values, delimiter) { + return values && values.length + ? values.split(delimiter || "\n").filter(Boolean) + : []; + } +}); diff --git a/app/assets/javascripts/admin/components/site-settings/simple-list.js b/app/assets/javascripts/admin/components/site-settings/simple-list.js new file mode 100644 index 0000000000..aab078bbe3 --- /dev/null +++ b/app/assets/javascripts/admin/components/site-settings/simple-list.js @@ -0,0 +1,11 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; + +export default Component.extend({ + inputDelimiter: "|", + + @action + onChange(value) { + this.set("value", value.join(this.inputDelimiter || "\n")); + } +}); diff --git a/app/assets/javascripts/admin/components/themes-list-item.js b/app/assets/javascripts/admin/components/themes-list-item.js index c292b80f99..56db74e7e8 100644 --- a/app/assets/javascripts/admin/components/themes-list-item.js +++ b/app/assets/javascripts/admin/components/themes-list-item.js @@ -4,7 +4,7 @@ import Component from "@ember/component"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { iconHTML } from "discourse-common/lib/icon-library"; import { escape } from "pretty-text/sanitizer"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; const MAX_COMPONENTS = 4; @@ -41,7 +41,7 @@ export default Component.extend({ animate(isInitial) { const $container = $(this.element); const $list = $(this.element.querySelector(".components-list")); - if ($list.length === 0 || ENV.environment === "test") { + if ($list.length === 0 || isTesting()) { return; } const duration = 300; diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-general.js b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js index 07a195b566..2030c1e613 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-general.js +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js @@ -8,6 +8,7 @@ import AdminDashboard from "admin/models/admin-dashboard"; import Report from "admin/models/report"; import PeriodComputationMixin from "admin/mixins/period-computation"; import { computed } from "@ember/object"; +import getURL from "discourse-common/lib/get-url"; function staticReport(reportType) { return computed("reports.[]", function() { @@ -20,7 +21,6 @@ export default Controller.extend(PeriodComputationMixin, { dashboardFetchedAt: null, exceptionController: inject("exception"), logSearchQueriesEnabled: setting("log_search_queries"), - basePath: Discourse.BaseUri, @discourseComputed("siteSettings.dashboard_general_tab_activity_metrics") activityMetrics(metrics) { @@ -107,7 +107,7 @@ export default Controller.extend(PeriodComputationMixin, { @discourseComputed trendingSearchDisabledLabel() { return I18n.t("admin.dashboard.reports.trending_search.disabled", { - basePath: Discourse.BaseUri + basePath: getURL("/") }); }, @@ -150,6 +150,6 @@ export default Controller.extend(PeriodComputationMixin, { }, _reportsForPeriodURL(period) { - return Discourse.getURL(`/admin?period=${period}`); + return getURL(`/admin?period=${period}`); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js index 95f820b598..f37d8606af 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; import PeriodComputationMixin from "admin/mixins/period-computation"; @@ -45,6 +46,6 @@ export default Controller.extend(PeriodComputationMixin, { }, _reportsForPeriodURL(period) { - return Discourse.getURL(`/admin/dashboard/moderation?period=${period}`); + return getURL(`/admin/dashboard/moderation?period=${period}`); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js b/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js index 031ad8ee8b..e6f20ee2f5 100644 --- a/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js +++ b/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js @@ -6,6 +6,7 @@ import Controller, { inject as controller } from "@ember/controller"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { extractDomainFromUrl } from "discourse/lib/utilities"; import EmberObject from "@ember/object"; +import { isAbsoluteURL } from "discourse-common/lib/get-url"; export default Controller.extend({ adminWebHooks: controller(), @@ -109,7 +110,7 @@ export default Controller.extend({ domain === "localhost" || domain.match(/192\.168\.\d+\.\d+/) || domain.match(/127\.\d+\.\d+\.\d+/) || - url.startsWith(Discourse.BaseUrl) + isAbsoluteURL(url) ) { return bootbox.confirm( I18n.t("admin.web_hooks.warn_local_payload_url"), diff --git a/app/assets/javascripts/admin/mixins/setting-component.js b/app/assets/javascripts/admin/mixins/setting-component.js index 83dcc31e65..699cecd32e 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js +++ b/app/assets/javascripts/admin/mixins/setting-component.js @@ -25,7 +25,8 @@ const CUSTOM_TYPES = [ "upload", "group_list", "tag_list", - "color" + "color", + "simple_list" ]; const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"]; diff --git a/app/assets/javascripts/admin/models/admin-user.js b/app/assets/javascripts/admin/models/admin-user.js index 9977fa8dd4..8dc7685cc1 100644 --- a/app/assets/javascripts/admin/models/admin-user.js +++ b/app/assets/javascripts/admin/models/admin-user.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { filter, or, gt, lt, not } from "@ember/object/computed"; @@ -43,7 +44,7 @@ const AdminUser = User.extend({ @discourseComputed bounceLink() { - return Discourse.getURL("/admin/email/bounced"); + return getURL("/admin/email/bounced"); }, canResetBounceScore: gt("bounce_score", 0), @@ -306,7 +307,7 @@ const AdminUser = User.extend({ type: "POST", data: { username_or_email: this.username } }) - .then(() => (document.location = Discourse.getURL("/"))) + .then(() => (document.location = getURL("/"))) .catch(e => { if (e.status === 404) { bootbox.alert(I18n.t("admin.impersonate.not_found")); @@ -395,11 +396,11 @@ const AdminUser = User.extend({ .then(function(data) { if (data.success) { if (data.username) { - document.location = Discourse.getURL( + document.location = getURL( `/admin/users/${user.get("id")}/${data.username}` ); } else { - document.location = Discourse.getURL("/admin/users/list/active"); + document.location = getURL("/admin/users/list/active"); } } else { bootbox.alert(I18n.t("admin.user.anonymize_failed")); @@ -456,7 +457,7 @@ const AdminUser = User.extend({ if (/^\/admin\/users\/list\//.test(location)) { document.location = location; } else { - document.location = Discourse.getURL("/admin/users/list/active"); + document.location = getURL("/admin/users/list/active"); } } else { bootbox.alert(I18n.t("admin.user.delete_failed")); diff --git a/app/assets/javascripts/admin/models/backup.js b/app/assets/javascripts/admin/models/backup.js index 45727a28b2..0b6f7122a4 100644 --- a/app/assets/javascripts/admin/models/backup.js +++ b/app/assets/javascripts/admin/models/backup.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { ajax } from "discourse/lib/ajax"; import { extractError } from "discourse/lib/ajax-error"; @@ -66,7 +67,7 @@ Backup.reopenClass({ bootbox.alert(result.message); } else { // redirect to homepage (session might be lost) - window.location = Discourse.getURL("/"); + window.location = getURL("/"); } }); } diff --git a/app/assets/javascripts/admin/models/email-log.js b/app/assets/javascripts/admin/models/email-log.js index c2eaaa26e6..1317df2943 100644 --- a/app/assets/javascripts/admin/models/email-log.js +++ b/app/assets/javascripts/admin/models/email-log.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import { ajax } from "discourse/lib/ajax"; import AdminUser from "admin/models/admin-user"; import EmberObject from "@ember/object"; @@ -13,7 +14,7 @@ EmailLog.reopenClass({ } if (attrs.post_url) { - attrs.post_url = Discourse.getURL(attrs.post_url); + attrs.post_url = getURL(attrs.post_url); } return this._super(attrs); diff --git a/app/assets/javascripts/admin/models/report.js b/app/assets/javascripts/admin/models/report.js index 2e0b448887..9d2f25f9a1 100644 --- a/app/assets/javascripts/admin/models/report.js +++ b/app/assets/javascripts/admin/models/report.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { makeArray } from "discourse-common/lib/helpers"; @@ -37,7 +38,7 @@ const Report = EmberObject.extend({ .locale("en") .format("YYYY-MM-DD"); - return Discourse.getURL( + return getURL( `/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}` ); }, @@ -335,7 +336,7 @@ const Report = EmberObject.extend({ avatar_template: row[properties.avatar] }); - const href = Discourse.getURL(`/admin/users/${userId}/${username}`); + const href = getURL(`/admin/users/${userId}/${username}`); const avatarImg = renderAvatar(user, { imageSize: "tiny", @@ -356,7 +357,7 @@ const Report = EmberObject.extend({ const formatedValue = () => { const topicId = row[properties.id]; - const href = Discourse.getURL(`/t/-/${topicId}`); + const href = getURL(`/t/-/${topicId}`); return `${escapeExpression(topicTitle)}`; }; @@ -370,7 +371,7 @@ const Report = EmberObject.extend({ const postTitle = row[properties.truncated_raw]; const postNumber = row[properties.number]; const topicId = row[properties.topic_id]; - const href = Discourse.getURL(`/t/-/${topicId}/${postNumber}`); + const href = getURL(`/t/-/${topicId}/${postNumber}`); return { property: properties.title, @@ -434,7 +435,7 @@ const Report = EmberObject.extend({ _linkLabel(properties, row) { const property = properties[0]; - const value = Discourse.getURL(row[property]); + const value = getURL(row[property]); const formatedValue = (href, anchor) => { return `${escapeExpression( anchor diff --git a/app/assets/javascripts/admin/routes/admin-backups.js b/app/assets/javascripts/admin/routes/admin-backups.js index 3d517acbf1..aa7c88aba9 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js +++ b/app/assets/javascripts/admin/routes/admin-backups.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import EmberObject from "@ember/object"; import DiscourseRoute from "discourse/routes/discourse"; @@ -40,7 +41,7 @@ export default DiscourseRoute.extend({ ); if (log.operation === "restore") { // redirect to homepage when the restore is done (session might be lost) - window.location = Discourse.getURL("/"); + window.location = getURL("/"); } } else { this.controllerFor("adminBackupsLogs") diff --git a/app/assets/javascripts/admin/templates/components/simple-list.hbs b/app/assets/javascripts/admin/templates/components/simple-list.hbs new file mode 100644 index 0000000000..3e78f77b05 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/simple-list.hbs @@ -0,0 +1,40 @@ +{{#if collection}} +
+ {{#each collection as |value index|}} +
+ {{d-button + action=(action "removeValue") + actionParam=value + icon="times" + class="remove-value-btn btn-small" + }} + + {{input + title=value + value=value + class="value-input" + focus-out=(action "changeValue" index) + }} +
+ {{/each}} +
+{{/if}} + +
+ {{input + type="text" + value=newValue + placeholderKey="admin.site_settings.simple_list.add_item" + class="add-value-input" + autocomplete="discourse" + autocorrect="off" + autocapitalize="off"}} + + {{d-button + action=(action "addValue") + actionParam=newValue + disabled=inputEmpty + icon="plus" + class="add-value-btn btn-small" + }} +
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/simple-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/simple-list.hbs new file mode 100644 index 0000000000..3eca57db83 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/site-settings/simple-list.hbs @@ -0,0 +1,3 @@ +{{simple-list values=value inputDelimiter=inputDelimiter onChange=(action "onChange")}} +{{setting-validation-message message=validationMessage}} +
{{html-safe setting.description}}
diff --git a/app/assets/javascripts/admin/templates/dashboard_general.hbs b/app/assets/javascripts/admin/templates/dashboard_general.hbs index 5566271a20..5d442f876b 100644 --- a/app/assets/javascripts/admin/templates/dashboard_general.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_general.hbs @@ -147,7 +147,7 @@ filters=trendingSearchFilters isEnabled=logSearchQueriesEnabled disabledLabel=trendingSearchDisabledLabel}} - {{html-safe (i18n "admin.dashboard.reports.trending_search.more" basePath=basePath)}} + {{html-safe (i18n "admin.dashboard.reports.trending_search.more" basePath=(base-url))}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs index de71c0bfb9..74738f2080 100644 --- a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs +++ b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs @@ -153,6 +153,7 @@ {{i18n "admin.user.tl3_requirements.locked_will_not_be_promoted"}} {{else}} {{d-icon "times"}} + {{i18n "admin.user.tl3_requirements.does_not_qualify"}} {{/if}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse-common/addon/config/environment.js b/app/assets/javascripts/discourse-common/addon/config/environment.js index 8d684bf5a4..b580231ee7 100644 --- a/app/assets/javascripts/discourse-common/addon/config/environment.js +++ b/app/assets/javascripts/discourse-common/addon/config/environment.js @@ -1,9 +1,23 @@ export const INPUT_DELAY = 250; -let environment = Ember.testing ? "test" : "development"; +let environment = "unknown"; -export function isTesting() { - return environment === "test"; +export function setEnvironment(e) { + if (isTesting()) { + environment = "testing"; + } else { + environment = e; + } } -export default { environment }; +export function isTesting() { + return Ember.testing; +} + +export function isDevelopment() { + return environment === "development"; +} + +export function isProduction() { + return environment === "production"; +} diff --git a/app/assets/javascripts/discourse-common/addon/helpers/get-url.js b/app/assets/javascripts/discourse-common/addon/helpers/get-url.js index e3847c4bde..f221c20c77 100644 --- a/app/assets/javascripts/discourse-common/addon/helpers/get-url.js +++ b/app/assets/javascripts/discourse-common/addon/helpers/get-url.js @@ -2,3 +2,4 @@ import { registerUnbound } from "discourse-common/lib/helpers"; import getUrl from "discourse-common/lib/get-url"; registerUnbound("get-url", value => getUrl(value)); +registerUnbound("base-url", () => getUrl("")); 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 4a3a20d0d8..a74ea5ff64 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/get-url.js +++ b/app/assets/javascripts/discourse-common/addon/lib/get-url.js @@ -1,9 +1,10 @@ -let baseUri; +let cdn, baseUrl, baseUri; +let S3BaseUrl, S3CDN; export default function getURL(url) { if (!url) return url; - if (!baseUri) { + if (baseUri === undefined) { baseUri = $('meta[name="discourse-base-uri"]').attr("content") || ""; } @@ -17,3 +18,42 @@ export default function getURL(url) { return baseUri + url; } + +export function getURLWithCDN(url) { + url = getURL(url); + // only relative urls + if (cdn && /^\/[^\/]/.test(url)) { + url = cdn + url; + } else if (S3CDN) { + url = url.replace(S3BaseUrl, S3CDN); + } + return url; +} + +export function getAbsoluteURL(path) { + return baseUrl + path; +} + +export function isAbsoluteURL(url) { + return url.startsWith(baseUrl); +} + +export function withoutPrefix(path) { + const rootURL = (!baseUri ? "/" : baseUri).replace(/\/$/, ""); + return path.replace(rootURL, ""); +} + +export function setPrefix(configBaseUri) { + baseUri = configBaseUri; +} + +export function setupURL(configCdn, configBaseUrl, configBaseUri) { + cdn = configCdn; + baseUrl = configBaseUrl; + baseUri = configBaseUri; +} + +export function setupS3CDN(configS3BaseUrl, configS3CDN) { + S3BaseUrl = configS3BaseUrl; + S3CDN = configS3CDN; +} diff --git a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js index ef60571cec..8633701c1e 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js +++ b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js @@ -1,10 +1,13 @@ import I18n from "I18n"; import { h } from "virtual-dom"; import attributeHook from "discourse-common/lib/attribute-hook"; +import { isDevelopment } from "discourse-common/config/environment"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; let _renderers = []; +let warnMissingIcons = true; + const REPLACEMENTS = { "d-tracking": "bell", "d-muted": "discourse-bell-slash", @@ -45,6 +48,14 @@ export function replaceIcon(source, destination) { REPLACEMENTS[source] = destination; } +export function disableMissingIconWarning() { + warnMissingIcons = false; +} + +export function enableMissingIconWarning() { + warnMissingIcons = false; +} + export function renderIcon(renderType, id, params) { for (let i = 0; i < _renderers.length; i++) { let renderer = _renderers[i]; @@ -105,8 +116,8 @@ function iconClasses(icon, params) { function warnIfMissing(id) { if ( typeof Discourse !== "undefined" && - Discourse.Environment === "development" && - !Discourse.disableMissingIconWarning && + isDevelopment() && + warnMissingIcons && Discourse.SvgIconList && Discourse.SvgIconList.indexOf(id) === -1 ) { diff --git a/app/assets/javascripts/discourse/app/adapters/topic-list.js b/app/assets/javascripts/discourse/app/adapters/topic-list.js index bd45b85e09..359d7ee05a 100644 --- a/app/assets/javascripts/discourse/app/adapters/topic-list.js +++ b/app/assets/javascripts/discourse/app/adapters/topic-list.js @@ -1,10 +1,11 @@ +import getURL from "discourse-common/lib/get-url"; import { ajax } from "discourse/lib/ajax"; import RestAdapter from "discourse/adapters/rest"; import PreloadStore from "discourse/lib/preload-store"; export function finderFor(filter, params) { return function() { - let url = Discourse.getURL("/") + filter + ".json"; + let url = getURL("/") + filter + ".json"; if (params) { const keys = Object.keys(params), diff --git a/app/assets/javascripts/discourse/app/app.js b/app/assets/javascripts/discourse/app/app.js index b484afcf52..06e5064e3e 100644 --- a/app/assets/javascripts/discourse/app/app.js +++ b/app/assets/javascripts/discourse/app/app.js @@ -4,6 +4,8 @@ import { computed } from "@ember/object"; import { buildResolver } from "discourse-common/resolver"; import { bind } from "@ember/runloop"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import { default as getURL, getURLWithCDN } from "discourse-common/lib/get-url"; +import deprecated from "discourse-common/lib/deprecated"; const _pluginCallbacks = []; @@ -42,26 +44,19 @@ const Discourse = Application.extend({ }, getURL(url) { - if (!url) return url; - - // if it's a non relative URL, return it. - if (url !== "/" && !/^\/[^\/]/.test(url)) return url; - - if (url[0] !== "/") url = "/" + url; - if (url.startsWith(this.BaseUri)) return url; - - return this.BaseUri + url; + deprecated( + "Import `getURL` from `discourse-common/lib/get-url` instead of `Discourse.getURL`", + { since: "2.5", dropFrom: "2.6" } + ); + return getURL(url); }, getURLWithCDN(url) { - url = this.getURL(url); - // only relative urls - if (this.CDN && /^\/[^\/]/.test(url)) { - url = this.CDN + url; - } else if (this.S3CDN) { - url = url.replace(this.S3BaseUrl, this.S3CDN); - } - return url; + deprecated( + "Import `getURLWithCDN` from `discourse-common/lib/get-url` instead of `Discourse.getURLWithCDN`", + { since: "2.5", dropFrom: "2.6" } + ); + return getURLWithCDN(url); }, Resolver: buildResolver("discourse"), diff --git a/app/assets/javascripts/discourse/app/components/cdn-img.js b/app/assets/javascripts/discourse/app/components/cdn-img.js index a5c7a2642a..e343eaea1f 100644 --- a/app/assets/javascripts/discourse/app/components/cdn-img.js +++ b/app/assets/javascripts/discourse/app/components/cdn-img.js @@ -1,3 +1,4 @@ +import { getURLWithCDN } from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; import { htmlSafe } from "@ember/template"; @@ -7,7 +8,7 @@ export default Component.extend({ @discourseComputed("src") cdnSrc(src) { - return Discourse.getURLWithCDN(src); + return getURLWithCDN(src); }, @discourseComputed("width", "height") diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index de6fa4344a..e33973cd44 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { debounce, later, next, schedule, throttle } from "@ember/runloop"; import Component from "@ember/component"; @@ -44,7 +45,7 @@ import { cacheShortUploadUrl, resolveAllShortUrls } from "pretty-text/upload-short-url"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; const REBUILD_SCROLL_MAP_EVENTS = ["composer:resized", "composer:typed-reply"]; @@ -650,9 +651,7 @@ export default Component.extend({ const $element = $(this.element); $element.fileupload({ - url: Discourse.getURL( - `/uploads.json?client_id=${this.messageBus.clientId}` - ), + url: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`), dataType: "json", pasteZone: $element }); @@ -847,7 +846,7 @@ export default Component.extend({ // need to wait a bit for the "slide down" transition of the composer later( () => this.appEvents.trigger("composer:closed"), - ENV.environment === "test" ? 0 : 400 + isTesting() ? 0 : 400 ); }); @@ -1001,7 +1000,7 @@ export default Component.extend({ ); // Short upload urls need resolution - resolveAllShortUrls(ajax, this.siteSettings, ".d-editor-preview-wrapper"); + resolveAllShortUrls(ajax, this.siteSettings, $preview[0]); if (this._enableAdvancedEditorPreviewSync()) { this._syncScroll( diff --git a/app/assets/javascripts/discourse/app/components/composer-title.js b/app/assets/javascripts/discourse/app/components/composer-title.js index 7ee2cb513a..ed037bd55d 100644 --- a/app/assets/javascripts/discourse/app/components/composer-title.js +++ b/app/assets/javascripts/discourse/app/components/composer-title.js @@ -6,7 +6,7 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { load } from "pretty-text/oneboxer"; import { lookupCache } from "pretty-text/oneboxer-cache"; import { ajax } from "discourse/lib/ajax"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; import EmberObject from "@ember/object"; import putCursorAtEnd from "discourse/lib/put-cursor-at-end"; @@ -76,7 +76,7 @@ export default Component.extend({ return; } - if (ENV.environment === "test") { + if (isTesting()) { next(() => // not ideal but we don't want to run this in current // runloop to avoid an error in console diff --git a/app/assets/javascripts/discourse/app/components/cook-text.js b/app/assets/javascripts/discourse/app/components/cook-text.js index f3ad48f549..234d0906d6 100644 --- a/app/assets/javascripts/discourse/app/components/cook-text.js +++ b/app/assets/javascripts/discourse/app/components/cook-text.js @@ -16,7 +16,7 @@ const CookText = Component.extend({ next(() => window .requireModule("pretty-text/upload-short-url") - .resolveAllShortUrls(ajax, this.siteSettings) + .resolveAllShortUrls(ajax, this.siteSettings, this.element) ); }); } diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 184b7f93a6..f979d7cc72 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -9,7 +9,7 @@ import discourseComputed, { } from "discourse-common/utils/decorators"; import { categoryHashtagTriggerRule } from "discourse/lib/category-hashtags"; import { search as searchCategoryTag } from "discourse/lib/category-tag-search"; -import { cookAsync } from "discourse/lib/text"; +import { generateCookFunction } from "discourse/lib/text"; import { getRegister } from "discourse-common/lib/get-owner"; import { findRawTemplate } from "discourse-common/lib/raw-templates"; import { siteDir } from "discourse/lib/text-direction"; @@ -28,7 +28,7 @@ import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji"; import { emojiUrlFor } from "discourse/lib/text"; import showModal from "discourse/lib/show-modal"; import { Promise } from "rsvp"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; // Our head can be a static string or a function that returns a string // based on input (like for numbered lists). @@ -344,18 +344,32 @@ export default Component.extend({ return toolbar; }, + cachedCookAsync(text) { + if (this._cachedCookFunction) { + return Promise.resolve(this._cachedCookFunction(text)); + } + + const markdownOptions = this.markdownOptions || {}; + return generateCookFunction(markdownOptions).then(cook => { + this._cachedCookFunction = cook; + return cook(text); + }); + }, + _updatePreview() { if (this._state !== "inDOM") { return; } const value = this.value; - const markdownOptions = this.markdownOptions || {}; - cookAsync(value, markdownOptions).then(cooked => { + this.cachedCookAsync(value).then(cooked => { if (this.isDestroyed) { return; } + + if (this.preview === cooked) return; + this.set("preview", cooked); schedule("afterRender", () => { if (this._state !== "inDOM") { @@ -378,7 +392,7 @@ export default Component.extend({ } // Debouncing in test mode is complicated - if (ENV.environment === "test") { + if (isTesting()) { this._updatePreview(); } else { debounce(this, this._updatePreview, 30); @@ -425,7 +439,7 @@ export default Component.extend({ return false; } - const matches = /(?:^|[^a-z])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec( + const matches = /(?:^|[>.,\/#!$%^&*;:{}=\-_`~()])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec( text.substring(0, cp) ); diff --git a/app/assets/javascripts/discourse/app/components/discourse-tag-bound.js b/app/assets/javascripts/discourse/app/components/discourse-tag-bound.js index 13c8798631..becd4a1fa6 100644 --- a/app/assets/javascripts/discourse/app/components/discourse-tag-bound.js +++ b/app/assets/javascripts/discourse/app/components/discourse-tag-bound.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; @@ -13,6 +14,6 @@ export default Component.extend({ @discourseComputed("tagRecord.id") href(tagRecordId) { - return Discourse.getURL("/tag/" + tagRecordId); + return getURL("/tag/" + tagRecordId); } }); diff --git a/app/assets/javascripts/discourse/app/components/edit-category-general.js b/app/assets/javascripts/discourse/app/components/edit-category-general.js index a2a477f6a1..931e8923fb 100644 --- a/app/assets/javascripts/discourse/app/components/edit-category-general.js +++ b/app/assets/javascripts/discourse/app/components/edit-category-general.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import { isEmpty } from "@ember/utils"; import { not } from "@ember/object/computed"; @@ -27,10 +28,10 @@ export default buildCategoryPanel("general", { }, canSelectParentCategory: not("category.isUncategorizedCategory"), - uncategorizedSiteSettingLink: Discourse.getURL( + uncategorizedSiteSettingLink: getURL( "/admin/site_settings/category/all_results?filter=allow_uncategorized_topics" ), - customizeTextContentLink: Discourse.getURL( + customizeTextContentLink: getURL( "/admin/customize/site_texts?q=uncategorized" ), diff --git a/app/assets/javascripts/discourse/app/components/edit-category-settings.js b/app/assets/javascripts/discourse/app/components/edit-category-settings.js index 5844e45d0c..5dc44ee02b 100644 --- a/app/assets/javascripts/discourse/app/components/edit-category-settings.js +++ b/app/assets/javascripts/discourse/app/components/edit-category-settings.js @@ -68,6 +68,13 @@ export default buildCategoryPanel("settings", { ); }, + @discourseComputed + availableListFilters() { + return ["all", "none"].map(p => { + return { name: I18n.t(`category.list_filters.${p}`), value: p }; + }); + }, + @discourseComputed searchPrioritiesOptions() { const options = []; @@ -101,11 +108,18 @@ export default buildCategoryPanel("settings", { .sort((a, b) => a.name.localeCompare(b.name)); }, + @discourseComputed("category.sort_ascending") + sortAscendingOption(sortAscending) { + if (sortAscending === "false") return false; + if (sortAscending === "true") return true; + return sortAscending; + }, + @discourseComputed sortAscendingOptions() { return [ - { name: I18n.t("category.sort_ascending"), value: "true" }, - { name: I18n.t("category.sort_descending"), value: "false" } + { name: I18n.t("category.sort_ascending"), value: true }, + { name: I18n.t("category.sort_descending"), value: false } ]; } }); diff --git a/app/assets/javascripts/discourse/app/components/email-dropdown.js b/app/assets/javascripts/discourse/app/components/email-dropdown.js new file mode 100644 index 0000000000..1a4046b240 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/email-dropdown.js @@ -0,0 +1,63 @@ +import { action, computed } from "@ember/object"; +import { inject as service } from "@ember/service"; +import I18n from "I18n"; +import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; + +export default DropdownSelectBoxComponent.extend({ + router: service(), + + classNames: ["email-dropdown"], + + selectKitOptions: { + icon: "wrench", + showFullTitle: false + }, + + content: computed("email", function() { + const content = []; + + if (this.email.primary) { + content.push({ + id: "updateEmail", + icon: "pencil-alt", + name: I18n.t("user.email.update_email"), + description: "" + }); + } + + if (!this.email.primary && this.email.confirmed) { + content.push({ + id: "setPrimaryEmail", + icon: "star", + name: I18n.t("user.email.set_primary"), + description: "" + }); + } + + if (!this.email.primary) { + content.push({ + id: "destroyEmail", + icon: "times", + name: I18n.t("user.email.destroy"), + description: "" + }); + } + + return content; + }), + + @action + onChange(id) { + switch (id) { + case "updateEmail": + this.router.transitionTo("preferences.email"); + break; + case "setPrimaryEmail": + this.setPrimaryEmail(this.email.email); + break; + case "destroyEmail": + this.destroyEmail(this.email.email); + break; + } + } +}); diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js index a77b564e59..3acf181622 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js @@ -10,7 +10,7 @@ import { emojiSearch } from "pretty-text/emoji"; import { safariHacksDisabled } from "discourse/lib/utilities"; -import ENV, { INPUT_DELAY } from "discourse-common/config/environment"; +import { isTesting, INPUT_DELAY } from "discourse-common/config/environment"; const PER_ROW = 11; function customEmojis() { @@ -525,7 +525,7 @@ export default Component.extend({ this.$picker.css(_.merge(attributes, options)); }; - if (ENV.environment === "test" || !this.automaticPositioning) { + if (isTesting() || !this.automaticPositioning) { desktopPositioning(); return; } diff --git a/app/assets/javascripts/discourse/app/components/emoji-uploader.js b/app/assets/javascripts/discourse/app/components/emoji-uploader.js index 9693811c15..0be91fec74 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-uploader.js +++ b/app/assets/javascripts/discourse/app/components/emoji-uploader.js @@ -1,4 +1,4 @@ -import { notEmpty, not } from "@ember/object/computed"; +import { notEmpty } from "@ember/object/computed"; import { action } from "@ember/object"; import Component from "@ember/component"; import discourseComputed from "discourse-common/utils/decorators"; @@ -11,7 +11,6 @@ export default Component.extend(UploadMixin, { uploadUrl: "/admin/customize/emojis", hasName: notEmpty("name"), hasGroup: notEmpty("group"), - addDisabled: not("hasName"), group: "default", emojiGroups: null, newEmojiGroups: null, @@ -23,6 +22,11 @@ export default Component.extend(UploadMixin, { this.set("newEmojiGroups", this.emojiGroups); }, + @discourseComputed("hasName", "uploading") + addDisabled() { + return !this.hasName || this.uploading; + }, + uploadOptions() { return { sequentialUploads: true }; }, diff --git a/app/assets/javascripts/discourse/app/components/flag-action-type.js b/app/assets/javascripts/discourse/app/components/flag-action-type.js index 93de11b8fb..50f041d9b2 100644 --- a/app/assets/javascripts/discourse/app/components/flag-action-type.js +++ b/app/assets/javascripts/discourse/app/components/flag-action-type.js @@ -20,7 +20,7 @@ export default Component.extend({ ) formattedName(name, nameKey, isCustomFlag, username) { if (isCustomFlag) { - return name.replace("{{username}}", username); + return name.replace(/{{username}}|%{username}/, username); } else { return I18n.t("flagging.formatted_name." + nameKey); } diff --git a/app/assets/javascripts/discourse/app/components/global-notice.js b/app/assets/javascripts/discourse/app/components/global-notice.js index 15eb1bf376..ced8c86bb2 100644 --- a/app/assets/javascripts/discourse/app/components/global-notice.js +++ b/app/assets/javascripts/discourse/app/components/global-notice.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { bind, cancel } from "@ember/runloop"; import Component from "@ember/component"; @@ -105,7 +106,7 @@ export default Component.extend({ if (this.site.wizard_required) { const requiredText = I18n.t("wizard_required", { - url: Discourse.getURL("/wizard") + url: getURL("/wizard") }); notices.push(Notice.create({ text: requiredText, id: "alert-wizard" })); } diff --git a/app/assets/javascripts/discourse/app/components/google-search.js b/app/assets/javascripts/discourse/app/components/google-search.js index 43db648e81..0054f2898b 100644 --- a/app/assets/javascripts/discourse/app/components/google-search.js +++ b/app/assets/javascripts/discourse/app/components/google-search.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import { alias } from "@ember/object/computed"; import Component from "@ember/component"; @@ -10,6 +11,6 @@ export default Component.extend({ @discourseComputed siteUrl() { - return `${location.protocol}//${location.host}${Discourse.getURL("/")}`; + return `${location.protocol}//${location.host}${getURL("/")}`; } }); diff --git a/app/assets/javascripts/discourse/app/components/group-flair-inputs.js b/app/assets/javascripts/discourse/app/components/group-flair-inputs.js index b5ecb07bfc..0e264cdad3 100644 --- a/app/assets/javascripts/discourse/app/components/group-flair-inputs.js +++ b/app/assets/javascripts/discourse/app/components/group-flair-inputs.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { debounce } from "@ember/runloop"; @@ -14,7 +15,7 @@ export default Component.extend({ @discourseComputed demoAvatarUrl() { - return Discourse.getURL("/images/avatar.png"); + return getURL("/images/avatar.png"); }, @discourseComputed("model.flair_type") @@ -105,7 +106,7 @@ export default Component.extend({ @action setFlairImage(upload) { this.model.setProperties({ - flair_url: Discourse.getURL(upload.url), + flair_url: getURL(upload.url), flair_upload_id: upload.id }); }, diff --git a/app/assets/javascripts/discourse/app/components/group-post.js b/app/assets/javascripts/discourse/app/components/group-post.js index 51bb53f0ea..cdbd44ba38 100644 --- a/app/assets/javascripts/discourse/app/components/group-post.js +++ b/app/assets/javascripts/discourse/app/components/group-post.js @@ -1,9 +1,10 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; export default Component.extend({ @discourseComputed("post.url") postUrl(url) { - return Discourse.getURL(url); + return getURL(url); } }); diff --git a/app/assets/javascripts/discourse/app/components/image-uploader.js b/app/assets/javascripts/discourse/app/components/image-uploader.js index 6db47602ad..48a8855889 100644 --- a/app/assets/javascripts/discourse/app/components/image-uploader.js +++ b/app/assets/javascripts/discourse/app/components/image-uploader.js @@ -1,3 +1,4 @@ +import { getURLWithCDN } from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import { isEmpty } from "@ember/utils"; import { next } from "@ember/runloop"; @@ -43,7 +44,7 @@ export default Component.extend(UploadMixin, { return "".htmlSafe(); } - return Discourse.getURLWithCDN(url); + return getURLWithCDN(url); }, @discourseComputed("imageCDNURL") diff --git a/app/assets/javascripts/discourse/app/components/invite-link-panel.js b/app/assets/javascripts/discourse/app/components/invite-link-panel.js new file mode 100644 index 0000000000..e2691e36fa --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/invite-link-panel.js @@ -0,0 +1,98 @@ +import I18n from "I18n"; +import Component from "@ember/component"; +import Group from "discourse/models/group"; +import { readOnly } from "@ember/object/computed"; +import { action } from "@ember/object"; +import discourseComputed from "discourse-common/utils/decorators"; +import Invite from "discourse/models/invite"; + +export default Component.extend({ + inviteModel: readOnly("panel.model.inviteModel"), + userInvitedShow: readOnly("panel.model.userInvitedShow"), + isStaff: readOnly("currentUser.staff"), + maxRedemptionAllowed: 5, + inviteExpiresAt: moment() + .add(1, "month") + .format("YYYY-MM-DD"), + + willDestroyElement() { + this._super(...arguments); + + this.reset(); + }, + + @discourseComputed("isStaff", "inviteModel.saving", "maxRedemptionAllowed") + disabled(isStaff, saving, canInviteTo, maxRedemptionAllowed) { + if (saving) return true; + if (!isStaff) return true; + if (maxRedemptionAllowed < 2) return true; + + return false; + }, + + groupFinder(term) { + return Group.findAll({ term, ignore_automatic: true }); + }, + + errorMessage: I18n.t("user.invited.invite_link.error"), + + reset() { + this.set("maxRedemptionAllowed", 5); + + this.inviteModel.setProperties({ + groupNames: null, + error: false, + saving: false, + finished: false, + inviteLink: null + }); + }, + + @action + generateMultipleUseInviteLink() { + if (this.disabled) { + return; + } + + const groupNames = this.get("inviteModel.groupNames"); + const maxRedemptionAllowed = this.maxRedemptionAllowed; + const inviteExpiresAt = this.inviteExpiresAt; + const userInvitedController = this.userInvitedShow; + const model = this.inviteModel; + model.setProperties({ saving: true, error: false }); + + return model + .generateMultipleUseInviteLink( + groupNames, + maxRedemptionAllowed, + inviteExpiresAt + ) + .then(result => { + model.setProperties({ + saving: false, + finished: true, + inviteLink: result + }); + + if (userInvitedController) { + Invite.findInvitedBy( + this.currentUser, + userInvitedController.filter + ).then(inviteModel => { + userInvitedController.setProperties({ + model: inviteModel, + totalInvites: inviteModel.invites.length + }); + }); + } + }) + .catch(e => { + if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { + this.set("errorMessage", e.jqXHR.responseJSON.errors[0]); + } else { + this.set("errorMessage", I18n.t("user.invited.invite_link.error")); + } + model.setProperties({ saving: false, error: true }); + }); + } +}); diff --git a/app/assets/javascripts/discourse/app/components/related-messages.js b/app/assets/javascripts/discourse/app/components/related-messages.js index 1e1457db7d..6381b3f9b8 100644 --- a/app/assets/javascripts/discourse/app/components/related-messages.js +++ b/app/assets/javascripts/discourse/app/components/related-messages.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; @@ -23,7 +24,7 @@ export default Component.extend({ @discourseComputed searchLink() { - return Discourse.getURL( + return getURL( `/search?expanded=true&q=%40${this.targetUser.username}%20in%3Apersonal-direct` ); }, diff --git a/app/assets/javascripts/discourse/app/components/reviewable-score.js b/app/assets/javascripts/discourse/app/components/reviewable-score.js index ee36959c61..d9b18492cf 100644 --- a/app/assets/javascripts/discourse/app/components/reviewable-score.js +++ b/app/assets/javascripts/discourse/app/components/reviewable-score.js @@ -10,7 +10,10 @@ export default Component.extend({ @discourseComputed("rs.score_type.title", "reviewable.target_created_by") title(title, targetCreatedBy) { if (title && targetCreatedBy) { - return title.replace("{{username}}", targetCreatedBy.username); + return title.replace( + /{{username}}|%{username}/, + targetCreatedBy.username + ); } return title; diff --git a/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js b/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js index cb9b3dac27..399e2a7e63 100644 --- a/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js +++ b/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js @@ -333,6 +333,13 @@ export default MountWidget.extend({ }); this.appEvents.on("post-stream:refresh", this, "_refresh"); + + // restore scroll position on browsers with aggressive BFCaches (like Safari) + window.onpageshow = function(event) { + if (event.persisted) { + DiscourseURL.routeTo(this.location.pathname); + } + }; }, willDestroyElement() { diff --git a/app/assets/javascripts/discourse/app/components/suggested-topics.js b/app/assets/javascripts/discourse/app/components/suggested-topics.js index e5c5a40f15..38ebaa1a5a 100644 --- a/app/assets/javascripts/discourse/app/components/suggested-topics.js +++ b/app/assets/javascripts/discourse/app/components/suggested-topics.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { computed, get } from "@ember/object"; @@ -25,7 +26,7 @@ export default Component.extend({ } const opts = { - latestLink: `
${I18n.t( + latestLink: `${I18n.t( "topic.view_latest_topics" )}` }; @@ -43,7 +44,7 @@ export default Component.extend({ } else { opts.catLink = '' + I18n.t("topic.browse_all_categories") + ""; diff --git a/app/assets/javascripts/discourse/app/components/topic-timer-info.js b/app/assets/javascripts/discourse/app/components/topic-timer-info.js index 6bbde2a217..953c356590 100644 --- a/app/assets/javascripts/discourse/app/components/topic-timer-info.js +++ b/app/assets/javascripts/discourse/app/components/topic-timer-info.js @@ -8,7 +8,7 @@ import { REMINDER_TYPE, DELETE_REPLIES_TYPE } from "discourse/controllers/edit-topic-timer"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; export default Component.extend({ classNames: ["topic-status-info"], @@ -92,7 +92,7 @@ export default Component.extend({ }); // TODO Sam: concerned this can cause a heavy rerender loop - if (ENV.environment !== "test") { + if (!isTesting()) { this._delayedRerender = later(() => { this.renderTopicTimer(); }, rerenderDelay); diff --git a/app/assets/javascripts/discourse/app/components/user-card-contents.js b/app/assets/javascripts/discourse/app/components/user-card-contents.js index ee19f26d82..220cadc923 100644 --- a/app/assets/javascripts/discourse/app/components/user-card-contents.js +++ b/app/assets/javascripts/discourse/app/components/user-card-contents.js @@ -1,3 +1,4 @@ +import { getURLWithCDN } from "discourse-common/lib/get-url"; import I18n from "I18n"; import { isEmpty } from "@ember/utils"; import { alias, gte, and, gt, not, or } from "@ember/object/computed"; @@ -153,7 +154,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { } const url = this.get("user.card_background_upload_url"); - const bg = isEmpty(url) ? "" : `url(${Discourse.getURLWithCDN(url)})`; + const bg = isEmpty(url) ? "" : `url(${getURLWithCDN(url)})`; thisElem.style.backgroundImage = bg; }, diff --git a/app/assets/javascripts/discourse/app/controllers/bookmark.js b/app/assets/javascripts/discourse/app/controllers/bookmark.js index aa731cb62d..05a4c26b53 100644 --- a/app/assets/javascripts/discourse/app/controllers/bookmark.js +++ b/app/assets/javascripts/discourse/app/controllers/bookmark.js @@ -270,11 +270,6 @@ export default Controller.extend(ModalFunctionality, { return this.nextMonth().format(I18n.t("dates.long_no_year")); }, - @discourseComputed() - basePath() { - return Discourse.BaseUri; - }, - @discourseComputed("userTimezone") userHasTimezoneSet(userTimezone) { return !_.isEmpty(userTimezone); diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index d030c88c3a..3ca2b70f36 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { isEmpty } from "@ember/utils"; import { and, or, alias, reads } from "@ember/object/computed"; @@ -23,7 +24,7 @@ import { emojiUnescape } from "discourse/lib/text"; import { shortDate } from "discourse/lib/formatter"; import { SAVE_LABELS, SAVE_ICONS } from "discourse/models/composer"; import { Promise } from "rsvp"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; import EmberObject, { computed, action } from "@ember/object"; import deprecated from "discourse-common/lib/deprecated"; @@ -71,7 +72,7 @@ function loadDraft(store, opts) { const _popupMenuOptionsCallbacks = []; -let _checkDraftPopup = ENV.environment !== "test"; +let _checkDraftPopup = !isTesting(); export function toggleCheckDraftPopup(enabled) { _checkDraftPopup = enabled; @@ -561,7 +562,7 @@ export default Controller.extend({ ) { groups.forEach(group => { let body; - const groupLink = Discourse.getURL(`/g/${group.name}/members`); + const groupLink = getURL(`/g/${group.name}/members`); if (group.max_mentions < group.user_count) { body = I18n.t("composer.group_mentioned_limit", { @@ -1109,7 +1110,7 @@ export default Controller.extend({ if (model.draftSaving) { // in test debounce is Ember.run, this will cause // an infinite loop - if (ENV.environment !== "test") { + if (!isTesting()) { this._saveDraftDebounce = debounce(this, this._saveDraft, 2000); } } else { @@ -1157,6 +1158,7 @@ export default Controller.extend({ const tagsArray = tags || []; if ( this.site.can_tag_topics && + !this.currentUser.staff && category && category.minimum_required_tags > tagsArray.length ) { diff --git a/app/assets/javascripts/discourse/app/controllers/create-account.js b/app/assets/javascripts/discourse/app/controllers/create-account.js index 75ec7b2a66..dc8ee4dbcf 100644 --- a/app/assets/javascripts/discourse/app/controllers/create-account.js +++ b/app/assets/javascripts/discourse/app/controllers/create-account.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { A } from "@ember/array"; import { isEmpty } from "@ember/utils"; @@ -86,10 +87,9 @@ export default Controller.extend( @discourseComputed disclaimerHtml() { return I18n.t("create_account.disclaimer", { - tos_link: this.get("siteSettings.tos_url") || Discourse.getURL("/tos"), + tos_link: this.get("siteSettings.tos_url") || getURL("/tos"), privacy_link: - this.get("siteSettings.privacy_policy_url") || - Discourse.getURL("/privacy") + this.get("siteSettings.privacy_policy_url") || getURL("/privacy") }); }, diff --git a/app/assets/javascripts/discourse/app/controllers/email-login.js b/app/assets/javascripts/discourse/app/controllers/email-login.js index 4000618587..6802271fd9 100644 --- a/app/assets/javascripts/discourse/app/controllers/email-login.js +++ b/app/assets/javascripts/discourse/app/controllers/email-login.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; @@ -7,7 +8,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; import { getWebauthnCredential } from "discourse/lib/webauthn"; export default Controller.extend({ - lockImageUrl: Discourse.getURL("/images/lock.svg"), + lockImageUrl: getURL("/images/lock.svg"), @discourseComputed("model") secondFactorRequired(model) { diff --git a/app/assets/javascripts/discourse/app/controllers/forgot-password.js b/app/assets/javascripts/discourse/app/controllers/forgot-password.js index 9821947e3b..334367a370 100644 --- a/app/assets/javascripts/discourse/app/controllers/forgot-password.js +++ b/app/assets/javascripts/discourse/app/controllers/forgot-password.js @@ -6,6 +6,7 @@ import { ajax } from "discourse/lib/ajax"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { escapeExpression } from "discourse/lib/utilities"; import { extractError } from "discourse/lib/ajax-error"; +import getURL from "discourse-common/lib/get-url"; export default Controller.extend(ModalFunctionality, { offerHelp: null, @@ -30,7 +31,7 @@ export default Controller.extend(ModalFunctionality, { help() { this.setProperties({ offerHelp: I18n.t("forgot_password.help", { - basePath: Discourse.BaseUri + basePath: getURL("/") }), helpSeen: true }); diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js index b421b77058..a38ec6e1a9 100644 --- a/app/assets/javascripts/discourse/app/controllers/invites-show.js +++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js @@ -1,16 +1,18 @@ import I18n from "I18n"; import { isEmpty } from "@ember/utils"; -import { alias, notEmpty } from "@ember/object/computed"; +import { alias, notEmpty, or, readOnly } from "@ember/object/computed"; import Controller from "@ember/controller"; import discourseComputed from "discourse-common/utils/decorators"; import getUrl from "discourse-common/lib/get-url"; import DiscourseURL from "discourse/lib/url"; import { ajax } from "discourse/lib/ajax"; +import { emailValid } from "discourse/lib/utilities"; import PasswordValidation from "discourse/mixins/password-validation"; import UsernameValidation from "discourse/mixins/username-validation"; import NameValidation from "discourse/mixins/name-validation"; import UserFieldsValidation from "discourse/mixins/user-fields-validation"; import { findAll as findLoginMethods } from "discourse/models/login-method"; +import EmberObject from "@ember/object"; export default Controller.extend( PasswordValidation, @@ -18,7 +20,7 @@ export default Controller.extend( NameValidation, UserFieldsValidation, { - invitedBy: alias("model.invited_by"), + invitedBy: readOnly("model.invited_by"), email: alias("model.email"), accountUsername: alias("model.username"), passwordRequired: notEmpty("accountPassword"), @@ -26,6 +28,21 @@ export default Controller.extend( errorMessage: null, userFields: null, inviteImageUrl: getUrl("/images/envelope.svg"), + isInviteLink: readOnly("model.is_invite_link"), + submitDisabled: or( + "emailValidation.failed", + "usernameValidation.failed", + "passwordValidation.failed", + "nameValidation.failed", + "userFieldsValidation.failed" + ), + rejectedEmails: null, + + init() { + this._super(...arguments); + + this.rejectedEmails = []; + }, @discourseComputed welcomeTitle() { @@ -44,21 +61,6 @@ export default Controller.extend( return findLoginMethods().length > 0; }, - @discourseComputed( - "usernameValidation.failed", - "passwordValidation.failed", - "nameValidation.failed", - "userFieldsValidation.failed" - ) - submitDisabled( - usernameFailed, - passwordFailed, - nameFailed, - userFieldsFailed - ) { - return usernameFailed || passwordFailed || nameFailed || userFieldsFailed; - }, - @discourseComputed fullnameRequired() { return ( @@ -66,6 +68,35 @@ export default Controller.extend( ); }, + @discourseComputed("email", "rejectedEmails.[]") + emailValidation(email, rejectedEmails) { + // If blank, fail without a reason + if (isEmpty(email)) { + return EmberObject.create({ + failed: true + }); + } + + if (rejectedEmails.includes(email)) { + return EmberObject.create({ + failed: true, + reason: I18n.t("user.email.invalid") + }); + } + + if (emailValid(email)) { + return EmberObject.create({ + ok: true, + reason: I18n.t("user.email.ok") + }); + } + + return EmberObject.create({ + failed: true, + reason: I18n.t("user.email.invalid") + }); + }, + actions: { submit() { const userFields = this.userFields; @@ -80,6 +111,7 @@ export default Controller.extend( url: `/invites/show/${this.get("model.token")}.json`, type: "PUT", data: { + email: this.email, username: this.accountUsername, name: this.accountName, password: this.accountPassword, @@ -97,6 +129,14 @@ export default Controller.extend( DiscourseURL.redirectTo(result.redirect_to); } } else { + if ( + result.errors && + result.errors.email && + result.errors.email.length > 0 && + result.values + ) { + this.rejectedEmails.pushObject(result.values.email); + } if ( result.errors && result.errors.password && diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index 4bc124b060..9edefd68ec 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { isEmpty } from "@ember/utils"; @@ -372,8 +373,8 @@ export default Controller.extend(ModalFunctionality, { // redirect client to the original URL $.removeCookie("destination_url"); window.location.href = destinationUrl; - } else if (window.location.pathname === Discourse.getURL("/login")) { - window.location = Discourse.getURL("/"); + } else if (window.location.pathname === getURL("/login")) { + window.location = getURL("/"); } else { window.location.reload(); } diff --git a/app/assets/javascripts/discourse/app/controllers/password-reset.js b/app/assets/javascripts/discourse/app/controllers/password-reset.js index 764396c21e..e14de1c1bd 100644 --- a/app/assets/javascripts/discourse/app/controllers/password-reset.js +++ b/app/assets/javascripts/discourse/app/controllers/password-reset.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { alias, or, readOnly } from "@ember/object/computed"; import Controller from "@ember/controller"; @@ -41,10 +42,10 @@ export default Controller.extend(PasswordValidation, { @discourseComputed("redirectTo") redirectHref(redirectTo) { - return Discourse.getURL(redirectTo || "/"); + return getURL(redirectTo || "/"); }, - lockImageUrl: Discourse.getURL("/images/lock.svg"), + lockImageUrl: getURL("/images/lock.svg"), actions: { submit() { diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/account.js b/app/assets/javascripts/discourse/app/controllers/preferences/account.js index 584440ed0c..6eecf3e7e9 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/account.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/account.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { not, or, gt } from "@ember/object/computed"; import Controller from "@ember/controller"; @@ -11,6 +12,7 @@ import { findAll } from "discourse/models/login-method"; import { ajax } from "discourse/lib/ajax"; import { userPath } from "discourse/lib/url"; import logout from "discourse/lib/logout"; +import EmberObject from "@ember/object"; // Number of tokens shown by default. const DEFAULT_AUTH_TOKENS_COUNT = 2; @@ -94,6 +96,39 @@ export default Controller.extend(CanCheckEmails, { disableConnectButtons: propertyNotEqual("model.id", "currentUser.id"), + @discourseComputed( + "model.email", + "model.secondary_emails.[]", + "model.unconfirmed_emails.[]" + ) + emails(primaryEmail, secondaryEmails, unconfirmedEmails) { + const emails = []; + + if (primaryEmail) { + emails.push( + EmberObject.create({ + email: primaryEmail, + primary: true, + confirmed: true + }) + ); + } + + if (secondaryEmails) { + secondaryEmails.forEach(email => { + emails.push(EmberObject.create({ email, confirmed: true })); + }); + } + + if (unconfirmedEmails) { + unconfirmedEmails.forEach(email => { + emails.push(EmberObject.create({ email })); + }); + } + + return emails.sort((a, b) => a.email.localeCompare(b.email)); + }, + @discourseComputed( "model.second_factor_enabled", "canCheckEmails", @@ -148,6 +183,26 @@ export default Controller.extend(CanCheckEmails, { .catch(popupAjaxError); }, + setPrimaryEmail(email) { + this.model.setPrimaryEmail(email).catch(popupAjaxError); + }, + + destroyEmail(email) { + this.model.destroyEmail(email); + }, + + resendConfirmationEmail(email) { + email.set("resending", true); + this.model + .addEmail(email.email) + .then(() => { + email.set("resent", true); + }) + .finally(() => { + email.set("resending", false); + }); + }, + changePassword() { if (!this.passwordProgress) { this.set( @@ -195,7 +250,7 @@ export default Controller.extend(CanCheckEmails, { () => { bootbox.alert( I18n.t("user.deleted_yourself"), - () => (window.location = Discourse.getURL("/")) + () => (window.location = getURL("/")) ); }, () => { diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/email.js b/app/assets/javascripts/discourse/app/controllers/preferences/email.js index f79e4d15d1..3365faf0f2 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/email.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/email.js @@ -7,10 +7,13 @@ import EmberObject from "@ember/object"; import { emailValid } from "discourse/lib/utilities"; export default Controller.extend({ + queryParams: ["new"], + taken: false, saving: false, error: false, success: false, + oldEmail: null, newEmail: null, newEmailEmpty: empty("newEmail"), @@ -23,16 +26,17 @@ export default Controller.extend({ "invalidEmail" ), - unchanged: propertyEqual("newEmailLower", "currentUser.email"), + unchanged: propertyEqual("newEmailLower", "oldEmail"), @discourseComputed("newEmail") newEmailLower(newEmail) { return newEmail.toLowerCase().trim(); }, - @discourseComputed("saving") - saveButtonText(saving) { + @discourseComputed("saving", "new") + saveButtonText(saving, isNew) { if (saving) return I18n.t("saving"); + if (isNew) return I18n.t("user.add_email.add"); return I18n.t("user.change"); }, @@ -41,9 +45,9 @@ export default Controller.extend({ return !emailValid(newEmail); }, - @discourseComputed("invalidEmail") - emailValidation(invalidEmail) { - if (invalidEmail) { + @discourseComputed("invalidEmail", "oldEmail", "newEmail") + emailValidation(invalidEmail, oldEmail, newEmail) { + if (invalidEmail && (oldEmail || newEmail)) { return EmberObject.create({ failed: true, reason: I18n.t("user.email.invalid") @@ -62,10 +66,13 @@ export default Controller.extend({ }, actions: { - changeEmail() { + saveEmail() { this.set("saving", true); - return this.model.changeEmail(this.newEmail).then( + return (this.new + ? this.model.addEmail(this.newEmail) + : this.model.changeEmail(this.newEmail) + ).then( () => this.set("success", true), e => { this.setProperties({ error: true, saving: false }); diff --git a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js index eb3a42fd14..7e8f67ebe2 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js +++ b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js @@ -1,5 +1,5 @@ import I18n from "I18n"; -import { equal, reads, gte } from "@ember/object/computed"; +import { equal, reads } from "@ember/object/computed"; import Controller from "@ember/controller"; import Invite from "discourse/models/invite"; import discourseDebounce from "discourse/lib/debounce"; @@ -35,21 +35,30 @@ export default Controller.extend({ }, INPUT_DELAY), inviteRedeemed: equal("filter", "redeemed"), + invitePending: equal("filter", "pending"), + + @discourseComputed("filter") + inviteLinks(filter) { + return filter === "links" && this.currentUser.staff; + }, @discourseComputed("filter") showBulkActionButtons(filter) { return ( filter === "pending" && this.model.invites.length > 4 && - this.currentUser.get("staff") + this.currentUser.staff ); }, canInviteToForum: reads("currentUser.can_invite_to_forum"), - canBulkInvite: reads("currentUser.admin"), + canSendInviteLink: reads("currentUser.staff"), - showSearch: gte("totalInvites", 10), + @discourseComputed("totalInvites", "inviteLinks") + showSearch(totalInvites, inviteLinks) { + return totalInvites >= 10 && !inviteLinks; + }, @discourseComputed("invitesCount.total", "invitesCount.pending") pendingLabel(invitesCountTotal, invitesCountPending) { @@ -73,6 +82,17 @@ export default Controller.extend({ } }, + @discourseComputed("invitesCount.total", "invitesCount.links") + linksLabel(invitesCountTotal, invitesCountLinks) { + if (invitesCountTotal > 50) { + return I18n.t("user.invited.links_tab_with_count", { + count: invitesCountLinks + }); + } else { + return I18n.t("user.invited.links_tab"); + } + }, + actions: { rescind(invite) { invite.rescind(); diff --git a/app/assets/javascripts/discourse/app/helpers/category-link.js b/app/assets/javascripts/discourse/app/helpers/category-link.js index 9530c26982..ff63695293 100644 --- a/app/assets/javascripts/discourse/app/helpers/category-link.js +++ b/app/assets/javascripts/discourse/app/helpers/category-link.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { get } from "@ember/object"; import { registerUnbound } from "discourse-common/lib/helpers"; @@ -104,9 +105,7 @@ function defaultCategoryLinkRenderer(category, opts) { let restricted = get(category, "read_restricted"); let url = opts.url ? opts.url - : Discourse.getURL( - `/c/${Category.slugFor(category)}/${get(category, "id")}` - ); + : getURL(`/c/${Category.slugFor(category)}/${get(category, "id")}`); let href = opts.link === false ? "" : url; let tagName = opts.link === false || opts.link === "false" ? "span" : "a"; let extraClasses = opts.extraClasses ? " " + opts.extraClasses : ""; diff --git a/app/assets/javascripts/discourse/app/initializers/live-development.js b/app/assets/javascripts/discourse/app/initializers/live-development.js index fe3d59875b..8c8a3f1287 100644 --- a/app/assets/javascripts/discourse/app/initializers/live-development.js +++ b/app/assets/javascripts/discourse/app/initializers/live-development.js @@ -1,6 +1,6 @@ import DiscourseURL from "discourse/lib/url"; import { currentThemeIds, refreshCSS } from "discourse/lib/theme-selector"; -import ENV from "discourse-common/config/environment"; +import { isDevelopment } from "discourse-common/config/environment"; import Handlebars from "handlebars"; // Use the message bus for live reloading of components for faster development. @@ -48,7 +48,7 @@ export default { }); // Useful to export this for debugging purposes - if (Discourse.Environment === "development" && ENV.environment !== "test") { + if (isDevelopment()) { window.DiscourseURL = DiscourseURL; } diff --git a/app/assets/javascripts/discourse/app/initializers/message-bus.js b/app/assets/javascripts/discourse/app/initializers/message-bus.js index f732065c0f..3422b6c405 100644 --- a/app/assets/javascripts/discourse/app/initializers/message-bus.js +++ b/app/assets/javascripts/discourse/app/initializers/message-bus.js @@ -1,6 +1,8 @@ +import getURL from "discourse-common/lib/get-url"; // Initialize the message bus to receive messages. import userPresent from "discourse/lib/user-presence"; import { handleLogoff } from "discourse/lib/ajax"; +import { isProduction } from "discourse-common/config/environment"; const LONG_POLL_AFTER_UNSEEN_TIME = 1200000; // 20 minutes @@ -32,7 +34,7 @@ export default { user = container.lookup("current-user:main"), siteSettings = container.lookup("site-settings:main"); - messageBus.alwaysLongPoll = Discourse.Environment === "development"; + messageBus.alwaysLongPoll = !isProduction(); messageBus.shouldLongPollCallback = () => userPresent(LONG_POLL_AFTER_UNSEEN_TIME); @@ -83,7 +85,7 @@ export default { return ajax(opts); }; - messageBus.baseUrl = Discourse.getURL("/"); + messageBus.baseUrl = getURL("/"); } if (user) { diff --git a/app/assets/javascripts/discourse/app/initializers/register-service-worker.js b/app/assets/javascripts/discourse/app/initializers/register-service-worker.js index fbfc4484de..6b5927d511 100644 --- a/app/assets/javascripts/discourse/app/initializers/register-service-worker.js +++ b/app/assets/javascripts/discourse/app/initializers/register-service-worker.js @@ -1,3 +1,6 @@ +import { isAbsoluteURL } from "discourse-common/lib/get-url"; +import getAbsoluteURL from "discourse-common/lib/get-url"; + export default { name: "register-service-worker", @@ -30,7 +33,7 @@ export default { }); navigator.serviceWorker - .register(`${Discourse.BaseUri}/${Discourse.ServiceWorkerURL}`) + .register(getAbsoluteURL(`/${Discourse.ServiceWorkerURL}`)) .catch(error => { // eslint-disable-next-line no-console console.info(`Failed to register Service Worker: ${error}`); @@ -46,7 +49,7 @@ export default { }, unregister(registration) { - if (registration.scope.startsWith(Discourse.BaseUrl)) { + if (isAbsoluteURL(registration.scope)) { registration.unregister(); } } diff --git a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js index 1482cf9d18..407ab97d8c 100644 --- a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js +++ b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js @@ -11,7 +11,7 @@ import { unsubscribe as unsubscribePushNotifications, isPushNotificationsEnabled } from "discourse/lib/push-notifications"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; export default { name: "subscribe-user-notifications", @@ -130,7 +130,7 @@ export default { Discourse.set("assetVersion", data) ); - if (ENV.environment !== "test") { + if (!isTesting()) { bus.subscribe(alertChannel(user), data => onNotification(data, user)); initDesktopNotifications(bus, appEvents); diff --git a/app/assets/javascripts/discourse/app/lib/ajax.js b/app/assets/javascripts/discourse/app/lib/ajax.js index 1eadcda626..da1af13206 100644 --- a/app/assets/javascripts/discourse/app/lib/ajax.js +++ b/app/assets/javascripts/discourse/app/lib/ajax.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { run } from "@ember/runloop"; import userPresent from "discourse/lib/user-presence"; @@ -159,7 +160,7 @@ export function ajax() { args.cache = true; // Disable JQuery cache busting param, which was created to deal with IE8 } - ajaxObj = $.ajax(Discourse.getURL(url), args); + ajaxObj = $.ajax(getURL(url), args); } let promise; @@ -169,7 +170,7 @@ export function ajax() { if ( args.type && args.type.toUpperCase() !== "GET" && - url !== Discourse.getURL("/clicks/track") && + url !== getURL("/clicks/track") && !Session.currentProp("csrfToken") ) { promise = new Promise((resolve, reject) => { 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 9a370f0e21..83ad3e96ab 100644 --- a/app/assets/javascripts/discourse/app/lib/category-tag-search.js +++ b/app/assets/javascripts/discourse/app/lib/category-tag-search.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseDebounce from "discourse/lib/debounce"; import { CANCELLED_STATUS } from "discourse/lib/autocomplete"; import Category from "discourse/models/category"; @@ -27,7 +28,7 @@ function searchTags(term, categories, limit) { ); const debouncedSearch = discourseDebounce((q, cats, resultFunc) => { - oldSearch = $.ajax(Discourse.getURL("/tags/filter/search"), { + oldSearch = $.ajax(getURL("/tags/filter/search"), { type: "GET", cache: true, data: { limit: limit, q } diff --git a/app/assets/javascripts/discourse/app/lib/click-track.js b/app/assets/javascripts/discourse/app/lib/click-track.js index 199a90ea43..d3a37441f7 100644 --- a/app/assets/javascripts/discourse/app/lib/click-track.js +++ b/app/assets/javascripts/discourse/app/lib/click-track.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { later } from "@ember/runloop"; import { ajax } from "discourse/lib/ajax"; @@ -5,7 +6,7 @@ import DiscourseURL from "discourse/lib/url"; import { wantsNewWindow } from "discourse/lib/intercept-click"; import { selectedText } from "discourse/lib/utilities"; import { Promise } from "rsvp"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; import User from "discourse/models/user"; export function isValidLink($link) { @@ -101,14 +102,14 @@ export default { let trackPromise = Promise.resolve(); if (tracking) { - if (ENV.environment !== "test" && navigator.sendBeacon) { + if (!isTesting() && navigator.sendBeacon) { const data = new FormData(); data.append("url", href); data.append("post_id", postId); data.append("topic_id", topicId); - navigator.sendBeacon(Discourse.getURL("/clicks/track"), data); + navigator.sendBeacon(getURL("/clicks/track"), data); } else { - trackPromise = ajax(Discourse.getURL("/clicks/track"), { + trackPromise = ajax(getURL("/clicks/track"), { type: "POST", data: { url: href, diff --git a/app/assets/javascripts/discourse/app/lib/computed.js b/app/assets/javascripts/discourse/app/lib/computed.js index 68eefd11da..b3a4eb2e2a 100644 --- a/app/assets/javascripts/discourse/app/lib/computed.js +++ b/app/assets/javascripts/discourse/app/lib/computed.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { computed } from "@ember/object"; import { htmlSafe as htmlSafeTemplateHelper } from "@ember/template"; @@ -126,7 +127,7 @@ export function fmt(...args) { export function url(...args) { const format = args.pop(); return computed(...args, function() { - return Discourse.getURL(addonFmt(format, ...args.map(a => this.get(a)))); + return getURL(addonFmt(format, ...args.map(a => this.get(a)))); }); } diff --git a/app/assets/javascripts/discourse/app/lib/discourse-location.js b/app/assets/javascripts/discourse/app/lib/discourse-location.js index 20e8ad8c5d..2c0466c739 100644 --- a/app/assets/javascripts/discourse/app/lib/discourse-location.js +++ b/app/assets/javascripts/discourse/app/lib/discourse-location.js @@ -1,6 +1,7 @@ import EmberObject from "@ember/object"; import { defaultHomepage } from "discourse/lib/utilities"; import { guidFor } from "@ember/object/internals"; +import { withoutPrefix } from "discourse-common/lib/get-url"; let popstateFired = false; const supportsHistoryState = window.history && "state" in window.history; const popstateCallbacks = []; @@ -62,10 +63,7 @@ const DiscourseLocation = EmberObject.extend({ @method getURL */ getURL() { - let url = this.location.pathname; - - url = url.replace(new RegExp(`^${Discourse.BaseUri}`), ""); - + let url = withoutPrefix(this.location.pathname); const search = this.location.search || ""; url += search; return url; diff --git a/app/assets/javascripts/discourse/app/lib/eyeline.js b/app/assets/javascripts/discourse/app/lib/eyeline.js index 53f2a8435a..7afe84fe0e 100644 --- a/app/assets/javascripts/discourse/app/lib/eyeline.js +++ b/app/assets/javascripts/discourse/app/lib/eyeline.js @@ -1,4 +1,4 @@ -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; import AppEvents from "discourse/services/app-events"; let _skipUpdate; @@ -9,7 +9,7 @@ export function configureEyeline(opts) { _skipUpdate = opts.skipUpdate; _rootElement = opts.rootElement; } else { - _skipUpdate = ENV.environment === "test"; + _skipUpdate = isTesting(); _rootElement = null; } } diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index 985d9fc1af..29cfaaf9d2 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -471,9 +471,7 @@ export default { }, _bindToPath(path, key) { - this.keyTrapper.bind(key, () => - DiscourseURL.routeTo(Discourse.BaseUri + path) - ); + this.keyTrapper.bind(key, () => DiscourseURL.routeTo(path)); }, _bindToClick(selector, binding) { diff --git a/app/assets/javascripts/discourse/app/lib/link-mentions.js b/app/assets/javascripts/discourse/app/lib/link-mentions.js index 02872edaec..700412a223 100644 --- a/app/assets/javascripts/discourse/app/lib/link-mentions.js +++ b/app/assets/javascripts/discourse/app/lib/link-mentions.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import { schedule } from "@ember/runloop"; import { ajax } from "discourse/lib/ajax"; import { userPath } from "discourse/lib/url"; @@ -15,7 +16,7 @@ function replaceSpan($e, username, opts) { extraClass = "notify"; } $e.replaceWith( - `@${username}` ); } else { diff --git a/app/assets/javascripts/discourse/app/lib/load-script.js b/app/assets/javascripts/discourse/app/lib/load-script.js index 7c8dfd851f..52babb1952 100644 --- a/app/assets/javascripts/discourse/app/lib/load-script.js +++ b/app/assets/javascripts/discourse/app/lib/load-script.js @@ -1,3 +1,4 @@ +import { default as getURL, getURLWithCDN } from "discourse-common/lib/get-url"; import { run } from "@ember/runloop"; import { ajax } from "discourse/lib/ajax"; import { Promise } from "rsvp"; @@ -51,9 +52,7 @@ export default function loadScript(url, opts) { // Scripts should always load from CDN // CSS is type text, to accept it from a CDN we would need to handle CORS - const fullUrl = opts.css - ? Discourse.getURL(url) - : Discourse.getURLWithCDN(url); + const fullUrl = opts.css ? getURL(url) : getURLWithCDN(url); $("script").each((i, tag) => { const src = tag.getAttribute("src"); diff --git a/app/assets/javascripts/discourse/app/lib/logout.js b/app/assets/javascripts/discourse/app/lib/logout.js index 23160f8e44..2d5bf9966b 100644 --- a/app/assets/javascripts/discourse/app/lib/logout.js +++ b/app/assets/javascripts/discourse/app/lib/logout.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import { isEmpty } from "@ember/utils"; import { findAll } from "discourse/models/login-method"; @@ -23,9 +24,9 @@ export default function logout(siteSettings, keyValueStore) { if (siteSettings.login_required && (sso || oneAuthenticator)) { // In this situation visiting most URLs will start the auth process again // Go to the `/login` page to avoid an immediate redirect - window.location.href = Discourse.getURL("/login"); + window.location.href = getURL("/login"); return; } - window.location.href = Discourse.getURL("/"); + window.location.href = getURL("/"); } diff --git a/app/assets/javascripts/discourse/app/lib/mobile.js b/app/assets/javascripts/discourse/app/lib/mobile.js index 511e269d83..b90ccc2424 100644 --- a/app/assets/javascripts/discourse/app/lib/mobile.js +++ b/app/assets/javascripts/discourse/app/lib/mobile.js @@ -1,4 +1,4 @@ -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; let mobileForced = false; @@ -12,7 +12,7 @@ const Mobile = { this.isMobileDevice = mobileForced || $html.hasClass("mobile-device"); this.mobileView = mobileForced || $html.hasClass("mobile-view"); - if (ENV.environment === "test" || mobileForced) { + if (isTesting() || mobileForced) { return; } diff --git a/app/assets/javascripts/discourse/app/lib/page-tracker.js b/app/assets/javascripts/discourse/app/lib/page-tracker.js index 4acbdc4ea3..3246f14ee8 100644 --- a/app/assets/javascripts/discourse/app/lib/page-tracker.js +++ b/app/assets/javascripts/discourse/app/lib/page-tracker.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import { next } from "@ember/runloop"; let _started = false; let cache = {}; @@ -28,7 +29,7 @@ export function startPageTracking(router, appEvents) { transition.urlMethod === "replace" && transition.queryParamsOnly; router.send("refreshTitle"); - const url = Discourse.getURL(router.get("url")); + const url = getURL(router.get("url")); // Refreshing the title is debounced, so we need to trigger this in the // next runloop to have the correct title. diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 101c97da4b..879eeac107 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -28,7 +28,10 @@ import { addTagsHtmlCallback } from "discourse/lib/render-tags"; import { addUserMenuGlyph } from "discourse/widgets/user-menu"; import { addPostClassesCallback } from "discourse/widgets/post"; import { addPostTransformCallback } from "discourse/widgets/post-stream"; -import { attachAdditionalPanel } from "discourse/widgets/header"; +import { + attachAdditionalPanel, + addToHeaderIcons +} from "discourse/widgets/header"; import { registerIconRenderer, replaceIcon @@ -1141,6 +1144,19 @@ class PluginApi { addCategoryLinkIcon(renderer) { addExtraIconRenderer(renderer); } + /** + * Adds a widget to the header-icon ul. The widget must already be created. You can create new widgets + * in a theme or plugin via an initializer prior to calling this function. + * + * ``` + * api.addToHeaderIcons( + * createWidget('some-widget') + * ``` + * + **/ + addToHeaderIcons(icon) { + addToHeaderIcons(icon); + } } let _pluginv01; diff --git a/app/assets/javascripts/discourse/app/lib/render-tag.js b/app/assets/javascripts/discourse/app/lib/render-tag.js index f1882a386e..da11af4669 100644 --- a/app/assets/javascripts/discourse/app/lib/render-tag.js +++ b/app/assets/javascripts/discourse/app/lib/render-tag.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import User from "discourse/models/user"; import { escapeExpression } from "discourse/lib/utilities"; @@ -24,7 +25,7 @@ export function defaultRenderTag(tag, params) { path = `/tag/${tag}`; } } - const href = path ? ` href='${Discourse.getURL(path)}' ` : ""; + const href = path ? ` href='${getURL(path)}' ` : ""; if (Discourse.SiteSettings.tag_style || params.style) { classes.push(params.style || Discourse.SiteSettings.tag_style); diff --git a/app/assets/javascripts/discourse/app/lib/search.js b/app/assets/javascripts/discourse/app/lib/search.js index 07eb6ad7bf..d0974eba05 100644 --- a/app/assets/javascripts/discourse/app/lib/search.js +++ b/app/assets/javascripts/discourse/app/lib/search.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { isEmpty } from "@ember/utils"; import EmberObject from "@ember/object"; @@ -67,7 +68,7 @@ export function translateResults(results, opts) { flairBgColor, fullName, name, - url: Discourse.getURL(`/g/${name}`) + url: getURL(`/g/${name}`) }; }) .compact(); @@ -77,7 +78,7 @@ export function translateResults(results, opts) { const tagName = escapeExpression(tag.name); return EmberObject.create({ id: tagName, - url: Discourse.getURL("/tag/" + tagName) + url: getURL("/tag/" + tagName) }); }) .compact(); diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js index 94d33b18d5..a6d5bf21ef 100644 --- a/app/assets/javascripts/discourse/app/lib/text.js +++ b/app/assets/javascripts/discourse/app/lib/text.js @@ -1,3 +1,4 @@ +import { getURLWithCDN } from "discourse-common/lib/get-url"; import PrettyText, { buildOptions } from "pretty-text/pretty-text"; import { performEmojiUnescape, buildEmojiUrl } from "pretty-text/emoji"; import WhiteLister from "pretty-text/white-lister"; @@ -7,8 +8,6 @@ import { formatUsername } from "discourse/lib/utilities"; import { Promise } from "rsvp"; import { htmlSafe } from "@ember/template"; -const getURLWithCDN = url => Discourse.getURLWithCDN(url); - function getOpts(opts) { const siteSettings = Discourse.__container__.lookup("site-settings:main"), site = Discourse.__container__.lookup("site:main"); @@ -39,6 +38,15 @@ export function cookAsync(text, options) { return loadMarkdownIt().then(() => cook(text, options)); } +// Warm up pretty text with a set of options and return a function +// which can be used to cook without rebuilding prettytext every time +export function generateCookFunction(options) { + return loadMarkdownIt().then(() => { + const prettyText = createPrettyText(options); + return text => prettyText.cook(text); + }); +} + export function sanitize(text, options) { return textSanitize(text, new WhiteLister(options)); } diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js index 9c39e41713..a083d8fe07 100644 --- a/app/assets/javascripts/discourse/app/lib/transform-post.js +++ b/app/assets/javascripts/discourse/app/lib/transform-post.js @@ -47,6 +47,7 @@ export function transformBasicPost(post) { new_user: post.trust_level === 0, name: post.name, user_title: post.user_title, + title_is_group: post.title_is_group, created_at: post.created_at, updated_at: post.updated_at, canDelete: post.can_delete, diff --git a/app/assets/javascripts/discourse/app/lib/url.js b/app/assets/javascripts/discourse/app/lib/url.js index 4086ca7b62..499b5d745c 100644 --- a/app/assets/javascripts/discourse/app/lib/url.js +++ b/app/assets/javascripts/discourse/app/lib/url.js @@ -5,6 +5,7 @@ import offsetCalculator from "discourse/lib/offset-calculator"; import LockOn from "discourse/lib/lock-on"; import { defaultHomepage } from "discourse/lib/utilities"; import User from "discourse/models/user"; +import { default as getURL, withoutPrefix } from "discourse-common/lib/get-url"; const rewrites = []; const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/; @@ -55,11 +56,11 @@ export function clearRewrites() { } export function userPath(subPath) { - return Discourse.getURL(subPath ? `/u/${subPath}` : "/u"); + return getURL(subPath ? `/u/${subPath}` : "/u"); } export function groupPath(subPath) { - return Discourse.getURL(subPath ? `/g/${subPath}` : "/g"); + return getURL(subPath ? `/g/${subPath}` : "/g"); } let _jumpScheduled = false; @@ -210,11 +211,10 @@ const DiscourseURL = EmberObject.extend({ } if (Discourse.get("requiresRefresh")) { - return redirectTo(Discourse.getURL(path)); + return redirectTo(getURL(path)); } const pathname = path.replace(/(https?\:)?\/\/[^\/]+/, ""); - const baseUri = Discourse.BaseUri; if (!DiscourseURL.isInternal(path)) { return redirectTo(path); @@ -241,13 +241,13 @@ const DiscourseURL = EmberObject.extend({ path = path.replace(/(https?\:)?\/\/[^\/]+/, ""); // Rewrite /my/* urls - let myPath = `${baseUri}/my/`; + let myPath = getURL("/my"); if (path.indexOf(myPath) === 0) { const currentUser = User.current(); if (currentUser) { path = path.replace( myPath, - userPath(currentUser.get("username_lower") + "/") + userPath(currentUser.get("username_lower")) ); } else { return redirectTo("/login-preferences"); @@ -256,11 +256,7 @@ const DiscourseURL = EmberObject.extend({ // handle prefixes if (path.indexOf("/") === 0) { - const rootURL = (baseUri === undefined ? "/" : baseUri).replace( - /\/$/, - "" - ); - path = path.replace(rootURL, ""); + path = withoutPrefix(path); } path = rewritePath(path); @@ -293,7 +289,7 @@ const DiscourseURL = EmberObject.extend({ }, routeToUrl(url, opts = {}) { - this.routeTo(Discourse.getURL(url), opts); + this.routeTo(getURL(url), opts); }, rewrite(regexp, replacement, opts) { @@ -301,7 +297,7 @@ const DiscourseURL = EmberObject.extend({ }, redirectTo(url) { - window.location = Discourse.getURL(url); + window.location = getURL(url); }, /** @@ -420,10 +416,8 @@ const DiscourseURL = EmberObject.extend({ // This has been extracted so it can be tested. origin() { - return ( - window.location.origin + - (Discourse.BaseUri === "/" ? "" : Discourse.BaseUri) - ); + let prefix = getURL("/"); + return window.location.origin + (prefix === "/" ? "" : prefix); }, // TODO: These container calls can be replaced eventually if we migrate this to a service diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index 688aa53929..92fa344134 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -2,6 +2,7 @@ import I18n from "I18n"; import { escape } from "pretty-text/sanitizer"; import toMarkdown from "discourse/lib/to-markdown"; import Handlebars from "handlebars"; +import { default as getURL, getURLWithCDN } from "discourse-common/lib/get-url"; const homepageSelector = "meta[name=discourse_current_homepage]"; @@ -55,18 +56,15 @@ export function getRawSize(size) { return size * Math.min(3, Math.max(1, Math.round(pixelRatio))); } -const getURLWithCDN = url => Discourse.getURLWithCDN(url); - -export function avatarImg(options, getURL) { - getURL = getURL || getURLWithCDN; - +export function avatarImg(options, customGetURL) { const size = translateSize(options.size); - const url = avatarUrl(options.avatarTemplate, size); + let path = avatarUrl(options.avatarTemplate, size); // We won't render an invalid url - if (!url || url.length === 0) { + if (!path || path.length === 0) { return ""; } + path = (customGetURL || getURLWithCDN)(path); const classes = "avatar" + (options.extraClasses ? " " + options.extraClasses : ""); @@ -77,19 +75,7 @@ export function avatarImg(options, getURL) { title = ` title='${escaped}' aria-label='${escaped}'`; } - return ( - "" - ); + return ``; } export function tinyAvatar(avatarTemplate, options) { @@ -99,7 +85,7 @@ export function tinyAvatar(avatarTemplate, options) { } export function postUrl(slug, topicId, postNumber) { - var url = Discourse.getURL("/t/"); + var url = getURL("/t/"); if (slug) { url += slug + "/"; } else { @@ -430,7 +416,7 @@ function reportToLogster(name, error) { stacktrace: error.stack }; - Ember.$.ajax(`${Discourse.BaseUri}/logs/report_js_error`, { + Ember.$.ajax(getURL("/logs/report_js_error"), { data, type: "POST", cache: false @@ -447,7 +433,7 @@ export function rescueThemeError(name, error, api) { return; } - const path = `${Discourse.BaseUri}/admin/customize/themes`; + const path = getURL(`/admin/customize/themes`); const message = I18n.t("themes.broken_theme_alert", { theme: name, path: `${path}` diff --git a/app/assets/javascripts/discourse/app/mapping-router.js b/app/assets/javascripts/discourse/app/mapping-router.js index 38b8eec72e..ca6728a1fb 100644 --- a/app/assets/javascripts/discourse/app/mapping-router.js +++ b/app/assets/javascripts/discourse/app/mapping-router.js @@ -1,14 +1,12 @@ import EmberRouter from "@ember/routing/router"; import { defaultHomepage } from "discourse/lib/utilities"; import { rewritePath } from "discourse/lib/url"; -import ENV from "discourse-common/config/environment"; import Site from "discourse/models/site"; - -const rootURL = Discourse.BaseUri; +import { isTesting } from "discourse-common/config/environment"; +import getURL from "discourse-common/lib/get-url"; const BareRouter = EmberRouter.extend({ - rootURL, - location: ENV.environment === "test" ? "none" : "discourse-location", + location: isTesting() ? "none" : "discourse-location", handleURL(url) { url = rewritePath(url); @@ -136,10 +134,10 @@ export function mapRoutes() { } }); - return BareRouter.extend().map(function() { + return BareRouter.extend({ + rootURL: getURL("/") + }).map(function() { tree.mapRoutes(this); this.route("unknown", { path: "*path" }); }); } - -export default BareRouter; diff --git a/app/assets/javascripts/discourse/app/mixins/scroll-top.js b/app/assets/javascripts/discourse/app/mixins/scroll-top.js index d636c2333f..9151ed192f 100644 --- a/app/assets/javascripts/discourse/app/mixins/scroll-top.js +++ b/app/assets/javascripts/discourse/app/mixins/scroll-top.js @@ -2,11 +2,11 @@ import { scheduleOnce } from "@ember/runloop"; import DiscourseURL from "discourse/lib/url"; import { deprecated } from "discourse/mixins/scroll-top"; import Mixin from "@ember/object/mixin"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; const context = { _scrollTop() { - if (ENV.environment === "test") { + if (isTesting()) { return; } $(document).scrollTop(0); diff --git a/app/assets/javascripts/discourse/app/mixins/upload.js b/app/assets/javascripts/discourse/app/mixins/upload.js index 85fdbc0031..b7394663a9 100644 --- a/app/assets/javascripts/discourse/app/mixins/upload.js +++ b/app/assets/javascripts/discourse/app/mixins/upload.js @@ -39,8 +39,10 @@ export default Mixin.create({ _initialize: on("didInsertElement", function() { const $upload = $(this.element); - const reset = () => + const reset = () => { this.setProperties({ uploading: false, uploadProgress: 0 }); + document.getElementsByClassName("hidden-upload-field")[0].value = ""; + }; const maxFiles = this.getWithDefault( "maxFiles", this.siteSettings.simultaneous_uploads diff --git a/app/assets/javascripts/discourse/app/models/badge.js b/app/assets/javascripts/discourse/app/models/badge.js index 5b9e46cef9..3ea5131ae1 100644 --- a/app/assets/javascripts/discourse/app/models/badge.js +++ b/app/assets/javascripts/discourse/app/models/badge.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import { none } from "@ember/object/computed"; import EmberObject from "@ember/object"; @@ -11,7 +12,7 @@ const Badge = RestModel.extend({ @discourseComputed url() { - return Discourse.getURL(`/badges/${this.id}/${this.slug}`); + return getURL(`/badges/${this.id}/${this.slug}`); }, updateFromJson(json) { diff --git a/app/assets/javascripts/discourse/app/models/bookmark.js b/app/assets/javascripts/discourse/app/models/bookmark.js index a55fb80ace..c7a3249144 100644 --- a/app/assets/javascripts/discourse/app/models/bookmark.js +++ b/app/assets/javascripts/discourse/app/models/bookmark.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import Category from "discourse/models/category"; import User from "discourse/models/user"; @@ -19,7 +20,7 @@ const Bookmark = RestModel.extend({ @computed get url() { - return Discourse.getURL(`/bookmarks/${this.id}`); + return getURL(`/bookmarks/${this.id}`); }, destroy() { @@ -37,7 +38,7 @@ const Bookmark = RestModel.extend({ // Helper to build a Url with a post number urlForPostNumber(postNumber) { - let url = Discourse.getURL(`/t/${this.topic_id}`); + let url = getURL(`/t/${this.topic_id}`); if (postNumber > 0) { url += `/${postNumber}`; } diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index 78802cba87..7d950cae2c 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import { get } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; @@ -96,7 +97,7 @@ const Category = RestModel.extend({ @discourseComputed("name") url() { - return Discourse.getURL(`/c/${Category.slugFor(this)}/${this.id}`); + return getURL(`/c/${Category.slugFor(this)}/${this.id}`); }, @discourseComputed @@ -187,7 +188,8 @@ const Category = RestModel.extend({ ), search_priority: this.search_priority, reviewable_by_group_name: this.reviewable_by_group_name, - read_only_banner: this.read_only_banner + read_only_banner: this.read_only_banner, + default_list_filter: this.default_list_filter }, type: id ? "PUT" : "POST" }); diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index 98ccdfaa6d..f4d302775a 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -374,7 +374,7 @@ const Composer = RestModel.extend({ "tags", "topicFirstPost", "minimumRequiredTags", - "isStaffUser" + "user.staff" ) cantSubmitPost( loading, diff --git a/app/assets/javascripts/discourse/app/models/invite.js b/app/assets/javascripts/discourse/app/models/invite.js index 7eb142c0ab..347ca71d68 100644 --- a/app/assets/javascripts/discourse/app/models/invite.js +++ b/app/assets/javascripts/discourse/app/models/invite.js @@ -10,7 +10,7 @@ const Invite = EmberObject.extend({ rescind() { ajax("/invites", { type: "DELETE", - data: { email: this.email } + data: { id: this.id } }); this.set("rescinded", true); }, @@ -42,7 +42,14 @@ Invite.reopenClass({ if (!isNone(search)) data.search = search; data.offset = offset || 0; - return ajax(userPath(`${user.username_lower}/invited.json`), { + let path; + if (filter === "links") { + path = userPath(`${user.username_lower}/invite_links.json`); + } else { + path = userPath(`${user.username_lower}/invited.json`); + } + + return ajax(path, { data }).then(result => { result.invites = result.invites.map(i => Invite.create(i)); diff --git a/app/assets/javascripts/discourse/app/models/login-method.js b/app/assets/javascripts/discourse/app/models/login-method.js index 0f2f1a46c0..0ef480e5ab 100644 --- a/app/assets/javascripts/discourse/app/models/login-method.js +++ b/app/assets/javascripts/discourse/app/models/login-method.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import EmberObject from "@ember/object"; @@ -33,7 +34,7 @@ const LoginMethod = EmberObject.extend({ return Promise.resolve(); } - let authUrl = Discourse.getURL(`/auth/${this.name}`); + let authUrl = getURL(`/auth/${this.name}`); if (reconnect) { params["reconnect"] = true; diff --git a/app/assets/javascripts/discourse/app/models/nav-item.js b/app/assets/javascripts/discourse/app/models/nav-item.js index 955e536e1e..bf41bdde66 100644 --- a/app/assets/javascripts/discourse/app/models/nav-item.js +++ b/app/assets/javascripts/discourse/app/models/nav-item.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { emojiUnescape } from "discourse/lib/text"; @@ -105,7 +106,7 @@ NavItem.reopenClass({ extraNavItemDescriptors: [], pathFor(filterType, context) { - let path = Discourse.getURL(""); + let path = getURL(""); let includesCategoryContext = false; let includesTagContext = false; diff --git a/app/assets/javascripts/discourse/app/models/published-page.js b/app/assets/javascripts/discourse/app/models/published-page.js index 7f0c12ddf5..91152fa04c 100644 --- a/app/assets/javascripts/discourse/app/models/published-page.js +++ b/app/assets/javascripts/discourse/app/models/published-page.js @@ -1,8 +1,9 @@ import RestModel from "discourse/models/rest"; import { computed } from "@ember/object"; +import { getAbsoluteURL } from "discourse-common/lib/get-url"; export default RestModel.extend({ url: computed("slug", function() { - return `${Discourse.BaseUrl}/pub/${this.slug}`; + return getAbsoluteURL(`/pub/${this.slug}`); }) }); diff --git a/app/assets/javascripts/discourse/app/models/site.js b/app/assets/javascripts/discourse/app/models/site.js index 03aee34519..8e6b8f2d2c 100644 --- a/app/assets/javascripts/discourse/app/models/site.js +++ b/app/assets/javascripts/discourse/app/models/site.js @@ -80,11 +80,6 @@ const Site = RestModel.extend({ return result; }, - @discourseComputed - baseUri() { - return Discourse.baseUri; - }, - // Returns it in the correct order, by setting @discourseComputed("categories.[]") categoriesList() { diff --git a/app/assets/javascripts/discourse/app/models/topic-details.js b/app/assets/javascripts/discourse/app/models/topic-details.js index d8d5ea0036..514db4d9f4 100644 --- a/app/assets/javascripts/discourse/app/models/topic-details.js +++ b/app/assets/javascripts/discourse/app/models/topic-details.js @@ -3,6 +3,8 @@ import discourseComputed from "discourse-common/utils/decorators"; import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import User from "discourse/models/user"; +import getURL from "discourse-common/lib/get-url"; + /** A model representing a Topic's details that aren't always present, such as a list of participants. When showing topics in lists and such this information should not be required. @@ -56,7 +58,7 @@ const TopicDetails = RestModel.extend({ } else { return I18n.t(localeString, { username: User.currentProp("username_lower"), - basePath: Discourse.BaseUri + basePath: getURL("/") }); } }, diff --git a/app/assets/javascripts/discourse/app/models/topic-list.js b/app/assets/javascripts/discourse/app/models/topic-list.js index 489c926de3..16b2b67c6b 100644 --- a/app/assets/javascripts/discourse/app/models/topic-list.js +++ b/app/assets/javascripts/discourse/app/models/topic-list.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import { notEmpty } from "@ember/object/computed"; import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; @@ -124,9 +125,9 @@ const TopicList = RestModel.extend({ this.topics.filter(topic => topic_ids.indexOf(topic.id) >= 0) ); - const url = `${Discourse.getURL("/")}${ - this.filter - }.json?topic_ids=${topic_ids.join(",")}`; + const url = `${getURL("/")}${this.filter}.json?topic_ids=${topic_ids.join( + "," + )}`; return ajax({ url, data: this.params }).then(result => { let i = 0; 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 65d1c21269..ee18ed4287 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -354,6 +354,10 @@ const TopicTrackingState = EmberObject.extend({ row.category_id = topic.category.id; } + if (topic.tags) { + row.tags = topic.tags; + } + tracker.states["t" + topic.id] = row; }); diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index af4b87fbe2..4f5305fec6 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import EmberObject from "@ember/object"; import { not, notEmpty, equal, and, or } from "@ember/object/computed"; @@ -25,7 +26,7 @@ import User from "discourse/models/user"; export function loadTopicView(topic, args) { const data = _.merge({}, args); - const url = `${Discourse.getURL("/t/")}${topic.id}`; + const url = `${getURL("/t/")}${topic.id}`; const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json"; delete data.nearPost; @@ -259,7 +260,7 @@ const Topic = RestModel.extend({ if (slug.trim().length === 0) { slug = "topic"; } - return `${Discourse.getURL("/t/")}${slug}/${id}`; + return `${getURL("/t/")}${slug}/${id}`; }, // Helper to build a Url with a post number @@ -739,7 +740,7 @@ Topic.reopenClass({ // Load a topic, but accepts a set of filters find(topicId, opts) { - let url = Discourse.getURL("/t/") + topicId; + let url = getURL("/t/") + topicId; if (opts.nearPost) { url += `/${opts.nearPost}`; } diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 6e287ccf72..0f67d613b2 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -1,3 +1,5 @@ +import { getURLWithCDN } from "discourse-common/lib/get-url"; +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { A } from "@ember/array"; import { isEmpty } from "@ember/utils"; @@ -109,11 +111,7 @@ const User = RestModel.extend({ if (isEmpty(bgUrl) || !Discourse.SiteSettings.allow_profile_backgrounds) { return "".htmlSafe(); } - return ( - "background-image: url(" + - Discourse.getURLWithCDN(bgUrl) + - ")" - ).htmlSafe(); + return ("background-image: url(" + getURLWithCDN(bgUrl) + ")").htmlSafe(); }, @discourseComputed() @@ -183,22 +181,22 @@ const User = RestModel.extend({ @discourseComputed() mutedTopicsPath() { return defaultHomepage() === "latest" - ? Discourse.getURL("/?state=muted") - : Discourse.getURL("/latest?state=muted"); + ? getURL("/?state=muted") + : getURL("/latest?state=muted"); }, @discourseComputed() watchingTopicsPath() { return defaultHomepage() === "latest" - ? Discourse.getURL("/?state=watching") - : Discourse.getURL("/latest?state=watching"); + ? getURL("/?state=watching") + : getURL("/latest?state=watching"); }, @discourseComputed() trackingTopicsPath() { return defaultHomepage() === "latest" - ? Discourse.getURL("/?state=tracking") - : Discourse.getURL("/latest?state=tracking"); + ? getURL("/?state=tracking") + : getURL("/latest?state=tracking"); }, @discourseComputed("username") @@ -248,6 +246,13 @@ const User = RestModel.extend({ }); }, + addEmail(email) { + return ajax(userPath(`${this.username_lower}/preferences/email`), { + type: "POST", + data: { email } + }); + }, + changeEmail(email) { return ajax(userPath(`${this.username_lower}/preferences/email`), { type: "PUT", @@ -377,6 +382,27 @@ const User = RestModel.extend({ }); }, + setPrimaryEmail(email) { + return ajax(userPath(`${this.username}/preferences/primary-email.json`), { + type: "PUT", + data: { email } + }).then(() => { + this.secondary_emails.removeObject(email); + this.secondary_emails.pushObject(this.email); + this.set("email", email); + }); + }, + + destroyEmail(email) { + return ajax(userPath(`${this.username}/preferences/email.json`), { + type: "DELETE", + data: { email } + }).then(() => { + this.secondary_emails.removeObject(email); + this.unconfirmed_emails.removeObject(email); + }); + }, + changePassword() { return ajax("/session/forgot_password", { dataType: "json", @@ -656,6 +682,17 @@ const User = RestModel.extend({ }); }, + generateMultipleUseInviteLink( + group_names, + max_redemptions_allowed, + expires_at + ) { + return ajax("/invites/link", { + type: "POST", + data: { group_names, max_redemptions_allowed, expires_at } + }); + }, + @observes("muted_category_ids") updateMutedCategories() { this.set("mutedCategories", Category.findByIds(this.muted_category_ids)); diff --git a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js index 494ed5e2e9..d8eb01dadc 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js @@ -2,7 +2,13 @@ import PreloadStore from "discourse/lib/preload-store"; import I18n from "I18n"; import Session from "discourse/models/session"; import RSVP from "rsvp"; -import { isTesting } from "discourse-common/config/environment"; +import { + setEnvironment, + isTesting, + isProduction +} from "discourse-common/config/environment"; +import { setupURL, setupS3CDN } from "discourse-common/lib/get-url"; +import deprecated from "discourse-common/lib/deprecated"; export default { name: "discourse-bootstrap", @@ -31,9 +37,29 @@ export default { } app.CDN = setupData.cdn; - app.BaseUrl = setupData.baseUrl; - app.BaseUri = setupData.baseUri; - app.Environment = setupData.environment; + + let baseUrl = setupData.baseUrl; + Object.defineProperty(app, "BaseUrl", { + get() { + deprecated(`use "get-url" helpers instead of Discourse.BaseUrl`, { + since: "2.5", + dropFrom: "2.6" + }); + return baseUrl; + } + }); + let baseUri = setupData.baseUri; + Object.defineProperty(app, "BaseUri", { + get() { + deprecated(`use "get-url" helpers instead of Discourse.BaseUri`, { + since: "2.5", + dropFrom: "2.6" + }); + return baseUri; + } + }); + setupURL(setupData.cdn, baseUrl, setupData.baseUri); + setEnvironment(setupData.environment); app.SiteSettings = PreloadStore.get("siteSettings"); app.ThemeSettings = PreloadStore.get("themeSettings"); app.LetterAvatarVersion = setupData.letterAvatarVersion; @@ -65,6 +91,7 @@ export default { if (setupData.s3BaseUrl) { app.S3CDN = setupData.s3Cdn; app.S3BaseUrl = setupData.s3BaseUrl; + setupS3CDN(setupData.s3BaseUrl, setupData.s3Cdn); } RSVP.configure("onerror", function(e) { @@ -73,7 +100,7 @@ export default { return; } - if (Discourse.Environment === "development") { + if (!isProduction()) { /* eslint-disable no-console */ if (e) { if (e.message || e.stack) { diff --git a/app/assets/javascripts/discourse/app/pre-initializers/dynamic-route-builders.js b/app/assets/javascripts/discourse/app/pre-initializers/dynamic-route-builders.js index 9c96d01e42..47626918f0 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/dynamic-route-builders.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/dynamic-route-builders.js @@ -13,6 +13,7 @@ export default { app.DiscoveryCategoryController = DiscoverySortableController.extend(); app.DiscoveryParentCategoryController = DiscoverySortableController.extend(); app.DiscoveryCategoryNoneController = DiscoverySortableController.extend(); + app.DiscoveryCategoryAllController = DiscoverySortableController.extend(); app.DiscoveryCategoryWithIDController = DiscoverySortableController.extend(); app.DiscoveryCategoryRoute = buildCategoryRoute("default"); @@ -20,6 +21,9 @@ export default { app.DiscoveryCategoryNoneRoute = buildCategoryRoute("default", { no_subcategories: true }); + app.DiscoveryCategoryAllRoute = buildCategoryRoute("default", { + no_subcategories: false + }); app.DiscoveryCategoryWithIDRoute = buildCategoryRoute("default"); const site = Site.current(); diff --git a/app/assets/javascripts/discourse/app/routes/app-route-map.js b/app/assets/javascripts/discourse/app/routes/app-route-map.js index fb13822bb2..9057ff2fe5 100644 --- a/app/assets/javascripts/discourse/app/routes/app-route-map.js +++ b/app/assets/javascripts/discourse/app/routes/app-route-map.js @@ -70,6 +70,7 @@ export default function() { // default filter for a category this.route("categoryNone", { path: "/c/*category_slug_path_with_id/none" }); + this.route("categoryAll", { path: "/c/*category_slug_path_with_id/all" }); this.route("category", { path: "/c/*category_slug_path_with_id" }); }); diff --git a/app/assets/javascripts/discourse/app/routes/application.js b/app/assets/javascripts/discourse/app/routes/application.js index b30dffebf8..cd9c6dd4a0 100644 --- a/app/assets/javascripts/discourse/app/routes/application.js +++ b/app/assets/javascripts/discourse/app/routes/application.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { once } from "@ember/runloop"; import DiscourseRoute from "discourse/routes/discourse"; @@ -46,8 +47,8 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { _collectTitleTokens(tokens) { tokens.push(this.siteTitle); if ( - (window.location.pathname === Discourse.getURL("/") || - window.location.pathname === Discourse.getURL("/login")) && + (window.location.pathname === getURL("/") || + window.location.pathname === getURL("/login")) && this.shortSiteDescription !== "" ) { tokens.push(this.shortSiteDescription); @@ -250,9 +251,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { handleShowLogin() { if (this.siteSettings.enable_sso) { const returnPath = encodeURIComponent(window.location.pathname); - window.location = Discourse.getURL( - "/session/sso?return_path=" + returnPath - ); + window.location = getURL("/session/sso?return_path=" + returnPath); } else { this._autoLogin("login", "login-modal", () => this.controllerFor("login").resetForm() @@ -263,9 +262,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { handleShowCreateAccount() { if (this.siteSettings.enable_sso) { const returnPath = encodeURIComponent(window.location.pathname); - window.location = Discourse.getURL( - "/session/sso?return_path=" + returnPath - ); + window.location = getURL("/session/sso?return_path=" + returnPath); } else { this._autoLogin("createAccount", "create-account"); } 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 18fe88124f..a28b6c8471 100644 --- a/app/assets/javascripts/discourse/app/routes/build-category-route.js +++ b/app/assets/javascripts/discourse/app/routes/build-category-route.js @@ -30,6 +30,11 @@ export default (filterArg, params) => { category, category_slug_path_with_id }); + } else if (modelParams.id === "all") { + modelParams.category_slug_path_with_id = [ + modelParams.parentSlug, + modelParams.slug + ].join("/"); } else { modelParams.category_slug_path_with_id = [ modelParams.parentSlug, @@ -61,12 +66,12 @@ export default (filterArg, params) => { const record = this.store.createRecord("category", result.category); record.setupGroupsAndPermissions(); this.site.updateCategory(record); - return { category: record }; + return { category: record, modelParams }; }); } if (category) { - return { category }; + return { category, modelParams }; } }, @@ -76,10 +81,24 @@ export default (filterArg, params) => { return; } - this._setupNavigation(model.category); + const { category, modelParams } = model; + + if ( + category.default_list_filter === "none" && + filterArg === "default" && + modelParams && + modelParams.id !== "all" + ) { + this.replaceWith("discovery.categoryNone", { + category, + category_slug_path_with_id: modelParams.category_slug_path_with_id + }); + } + + this._setupNavigation(category); return all([ - this._createSubcategoryList(model.category), - this._retrieveTopicList(model.category, transition) + this._createSubcategoryList(category), + this._retrieveTopicList(category, transition, modelParams) ]); }, @@ -113,11 +132,11 @@ export default (filterArg, params) => { return Promise.resolve(); }, - _retrieveTopicList(category, transition) { + _retrieveTopicList(category, transition, modelParams) { const listFilter = `c/${Category.slugFor(category)}/${ category.id }/l/${this.filter(category)}`, - findOpts = filterQueryParams(transition.to.queryParams, params), + findOpts = filterQueryParams(modelParams, params), extras = { cached: this.isPoppedState(transition) }; return findTopicList( @@ -158,7 +177,7 @@ export default (filterArg, params) => { category = model.category, canCreateTopic = topics.get("can_create_topic"), canCreateTopicOnCategory = - category.get("permission") === PermissionType.FULL, + canCreateTopic && category.get("permission") === PermissionType.FULL, filter = this.filter(category); this.controllerFor("navigation/category").setProperties({ diff --git a/app/assets/javascripts/discourse/app/routes/build-topic-route.js b/app/assets/javascripts/discourse/app/routes/build-topic-route.js index 8d34c8380e..f623bf0b5c 100644 --- a/app/assets/javascripts/discourse/app/routes/build-topic-route.js +++ b/app/assets/javascripts/discourse/app/routes/build-topic-route.js @@ -1,3 +1,4 @@ +import { isEmpty } from "@ember/utils"; import I18n from "I18n"; import DiscourseRoute from "discourse/routes/discourse"; import { queryParams } from "discourse/controllers/discovery-sortable"; @@ -12,7 +13,7 @@ function filterQueryParams(params, defaultParams) { if (params) { Object.keys(queryParams).forEach(function(opt) { - if (params[opt]) { + if (!isEmpty(params[opt])) { findOpts[opt] = params[opt]; } }); @@ -50,10 +51,10 @@ function findTopicList(store, tracking, filter, filterParams, extras) { // Clean up any string parameters that might slip through filterParams = filterParams || {}; - Object.keys(filterParams).forEach(function(k) { + Object.keys(filterParams).forEach(k => { const val = filterParams[k]; - if (val === "undefined" || val === "null" || val === "false") { - filterParams[k] = undefined; + if (val === "undefined" || val === "null") { + filterParams[k] = null; } }); diff --git a/app/assets/javascripts/discourse/app/routes/preferences-email.js b/app/assets/javascripts/discourse/app/routes/preferences-email.js index c3fb268c4d..f2d070ad00 100644 --- a/app/assets/javascripts/discourse/app/routes/preferences-email.js +++ b/app/assets/javascripts/discourse/app/routes/preferences-email.js @@ -13,7 +13,17 @@ export default RestrictedUserRoute.extend({ setupController: function(controller, model) { controller.reset(); - controller.setProperties({ model: model, newEmail: model.get("email") }); + controller.setProperties({ + model: model, + oldEmail: controller.new ? "" : model.get("email"), + newEmail: controller.new ? "" : model.get("email") + }); + }, + + resetController: function(controller, isExiting) { + if (isExiting) { + controller.set("new", undefined); + } }, // A bit odd, but if we leave to /preferences we need to re-render that outlet diff --git a/app/assets/javascripts/discourse/app/routes/topic-from-params.js b/app/assets/javascripts/discourse/app/routes/topic-from-params.js index bcf904ad38..2c028e42de 100644 --- a/app/assets/javascripts/discourse/app/routes/topic-from-params.js +++ b/app/assets/javascripts/discourse/app/routes/topic-from-params.js @@ -3,7 +3,7 @@ import { schedule } from "@ember/runloop"; import DiscourseRoute from "discourse/routes/discourse"; import DiscourseURL from "discourse/lib/url"; import Draft from "discourse/models/draft"; -import ENV from "discourse-common/config/environment"; +import { isTesting } from "discourse-common/config/environment"; // This route is used for retrieving a topic based on params export default DiscourseRoute.extend({ @@ -81,7 +81,7 @@ export default DiscourseRoute.extend({ } }) .catch(e => { - if (ENV.environment !== "test") { + if (!isTesting()) { // eslint-disable-next-line no-console console.log("Could not view topic", e); } diff --git a/app/assets/javascripts/discourse/app/routes/user-invited-show.js b/app/assets/javascripts/discourse/app/routes/user-invited-show.js index 3cd83f79ce..7af69e14df 100644 --- a/app/assets/javascripts/discourse/app/routes/user-invited-show.js +++ b/app/assets/javascripts/discourse/app/routes/user-invited-show.js @@ -30,18 +30,51 @@ export default DiscourseRoute.extend({ actions: { showInvite() { + const panels = [ + { + id: "invite", + title: "user.invited.single_user", + model: { + inviteModel: this.currentUser, + userInvitedShow: this.controllerFor("user-invited-show") + } + } + ]; + + if (this.get("currentUser.staff")) { + panels.push({ + id: "invite-link", + title: "user.invited.multiple_user", + model: { + inviteModel: this.currentUser, + userInvitedShow: this.controllerFor("user-invited-show") + } + }); + } + showModal("share-and-invite", { modalClass: "share-and-invite", - panels: [ - { - id: "invite", - title: "user.invited.create", - model: { - inviteModel: this.currentUser, - userInvitedShow: this.controllerFor("user-invited-show") - } + panels + }); + }, + + editInvite(inviteKey) { + const inviteLink = `${Discourse.BaseUrl}/invites/${inviteKey}`; + this.currentUser.setProperties({ finished: true, inviteLink }); + const panels = [ + { + id: "invite-link", + title: "user.invited.generate_link", + model: { + inviteModel: this.currentUser, + userInvitedShow: this.controllerFor("user-invited-show") } - ] + } + ]; + + showModal("share-and-invite", { + modalClass: "share-and-invite", + panels }); } } diff --git a/app/assets/javascripts/discourse/app/services/logs-notice.js b/app/assets/javascripts/discourse/app/services/logs-notice.js index c84dadb944..5faaaa2805 100644 --- a/app/assets/javascripts/discourse/app/services/logs-notice.js +++ b/app/assets/javascripts/discourse/app/services/logs-notice.js @@ -1,3 +1,4 @@ +import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; import { isEmpty } from "@ember/utils"; import EmberObject from "@ember/object"; @@ -42,7 +43,7 @@ const LogsNotice = EmberObject.extend({ ), rate, limit: siteSettingLimit, - url: Discourse.getURL("/logs") + url: getURL("/logs") }) ); }); diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs index ab17a307c3..5520c045a7 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs @@ -181,21 +181,44 @@ {{i18n "category.sort_order"}}
- {{combo-box valueProperty="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}} + {{combo-box + valueProperty="value" + content=availableSorts + value=category.sort_order + options=(hash + none="category.sort_options.default" + ) + onChange=(action (mut category.sort_order)) + }} {{#unless isDefaultSortOrder}} {{combo-box - castBoolean=true valueProperty="value" content=sortAscendingOptions - value=category.sort_ascending - none="category.sort_options.default" + value=sortAscendingOption options=(hash + none="category.sort_options.default" placementStrategy="absolute" ) + onChange=(action (mut category.sort_ascending)) }} {{/unless}}
+ +
+ +
+ {{combo-box + id="category-default-filter" + valueProperty="value" + content=availableListFilters + value=category.default_list_filter + }} +
+
+ {{#if isParentCategory}}