Version bump

This commit is contained in:
Neil Lalonde 2021-10-20 17:32:28 -04:00
commit 414d39f883
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
888 changed files with 25763 additions and 10828 deletions

View File

@ -1,4 +1,4 @@
name: Ember CLI tests
name: (experimental) Ember CLI tests (core)
on:
pull_request:
@ -9,10 +9,14 @@ on:
jobs:
build:
name: run
if: true
runs-on: ubuntu-latest
container: discourse/discourse_test:release
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
browser: ["Chrome", "Firefox", "Headless Firefox"]
steps:
- uses: actions/checkout@master
@ -43,5 +47,5 @@ jobs:
- name: Core QUnit
working-directory: ./app/assets/javascripts/discourse
run: yarn ember test
timeout-minutes: 30
run: sudo -E -u discourse -H yarn ember test --launch "${{ matrix.browser }}"
timeout-minutes: 60

View File

@ -0,0 +1,49 @@
name: (experimental) Ember CLI tests (plugins)
on:
schedule:
- cron: "0 0 * * *"
jobs:
build:
name: run
runs-on: ubuntu-latest
container: discourse/discourse_test:release
timeout-minutes: 60
steps:
- uses: actions/checkout@master
with:
fetch-depth: 1
- name: Setup Git
run: |
git config --global user.email "ci@ci.invalid"
git config --global user.name "Discourse CI"
- name: Get yarn cache directory
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Official Plugins Install
run: |
bundle config --local path vendor/bundle
bundle config --local deployment true
bundle config --local without development
bundle install --jobs 4
bundle clean
bundle exec rake plugin:install_all_official
- name: QUnit
working-directory: ./app/assets/javascripts/discourse
run: QUNIT_EMBER_CLI=1 sudo -E -u discourse -H rake plugin:qunit
timeout-minutes: 60

View File

@ -28,7 +28,6 @@ jobs:
build_type: [backend, frontend, annotations]
target: [core, plugins]
postgres: ["13"]
redis: ["6.x"]
exclude:
- build_type: annotations
target: plugins
@ -59,10 +58,9 @@ jobs:
git config --global user.email "ci@ci.invalid"
git config --global user.name "Discourse CI"
- name: Setup redis
uses: shogo82148/actions-setup-redis@v1
with:
redis-version: ${{ matrix.redis }}
- name: Start redis
run: |
redis-server /etc/redis/redis.conf &
- name: Bundler cache
uses: actions/cache@v2

View File

@ -210,6 +210,9 @@ gem 'gc_tracer', require: false, platform: :mri
# required for feed importing and embedding
gem 'ruby-readability', require: false
# rss gem is a bundled gem from Ruby 3 onwards
gem 'rss', require: false
gem 'stackprof', require: false, platform: :mri
gem 'memory_profiler', require: false, platform: :mri

View File

@ -53,8 +53,8 @@ GEM
rake (>= 10.4, < 14.0)
ast (2.4.2)
aws-eventstream (1.2.0)
aws-partitions (1.432.0)
aws-sdk-core (3.112.1)
aws-partitions (1.516.0)
aws-sdk-core (3.121.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
@ -66,10 +66,10 @@ GEM
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sdk-sns (1.38.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-sns (1.46.0)
aws-sdk-core (~> 3, >= 3.121.2)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.3)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
barber (0.12.2)
ember-source (>= 1.0, < 3.1)
@ -80,7 +80,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bootsnap (1.8.1)
bootsnap (1.9.1)
msgpack (~> 1.0)
builder (3.2.4)
bullet (6.1.5)
@ -129,14 +129,14 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.10.0)
excon (0.85.0)
excon (0.87.0)
execjs (2.8.1)
exifr (1.3.9)
fabrication (2.22.0)
faker (2.19.0)
i18n (>= 1.6, < 2)
fakeweb (1.3.0)
faraday (1.7.1)
faraday (1.8.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -173,7 +173,7 @@ GEM
http_accept_language (2.1.1)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
image_optim (0.30.0)
image_optim (0.31.0)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (>= 1.5, < 3)
@ -181,12 +181,13 @@ GEM
progress (~> 3.0, >= 3.0.1)
image_size (2.1.2)
in_threads (1.5.4)
ipaddr (1.2.2)
jmespath (1.4.0)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.5.1)
json (2.6.0)
json-schema (2.8.1)
addressable (>= 2.4)
json_schemer (0.2.18)
@ -194,7 +195,7 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.2.3)
jwt (2.3.0)
kgio (2.11.4)
libv8-node (15.14.0.1)
libv8-node (15.14.0.1-arm64-darwin-20)
@ -225,7 +226,7 @@ GEM
message_bus (3.3.6)
rack (>= 1.1.3)
method_source (1.0.0)
mini_mime (1.1.1)
mini_mime (1.1.2)
mini_portile2 (2.6.1)
mini_racer (0.4.0)
libv8-node (~> 15.14.0.0)
@ -244,14 +245,14 @@ GEM
multipart-post (2.1.1)
mustache (1.1.1)
nio4r (2.5.8)
nokogiri (1.12.4)
nokogiri (1.12.5)
mini_portile2 (~> 2.6.1)
racc (~> 1.4)
nokogiri (1.12.4-arm64-darwin)
nokogiri (1.12.5-arm64-darwin)
racc (~> 1.4)
nokogiri (1.12.4-x86_64-darwin)
nokogiri (1.12.5-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.12.4-x86_64-linux)
nokogiri (1.12.5-x86_64-linux)
racc (~> 1.4)
oauth (0.5.6)
oauth2 (1.4.7)
@ -283,12 +284,13 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
openssl (2.2.0)
openssl (2.2.1)
ipaddr
openssl-signature_algorithm (1.1.1)
openssl (~> 2.0)
optimist (3.0.1)
parallel (1.20.1)
parallel_tests (3.7.1)
parallel (1.21.0)
parallel_tests (3.7.3)
parallel
parser (3.0.2.0)
ast (~> 2.4.1)
@ -303,10 +305,10 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.4.0)
puma (5.5.2)
nio4r (~> 2.0)
r2 (0.2.7)
racc (1.5.2)
racc (1.6.0)
rack (2.2.3)
rack-mini-profiler (2.3.3)
rack (>= 1.2.0)
@ -323,7 +325,7 @@ GEM
activerecord (~> 6.0)
concurrent-ruby
railties (~> 6.0)
rails_multisite (3.0.0)
rails_multisite (3.1.0)
activerecord (> 5.0, < 7)
railties (> 5.0, < 7)
railties (6.1.4.1)
@ -343,7 +345,7 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.4.0)
redis (4.5.1)
redis-namespace (1.8.1)
redis (>= 3.0.4)
regexp_parser (2.1.1)
@ -380,28 +382,29 @@ GEM
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.2)
rss (0.2.9)
rexml
rswag-specs (2.4.0)
activesupport (>= 3.1, < 7.0)
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (1.20.0)
rubocop (1.22.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.9.1, < 2.0)
rubocop-ast (>= 1.12.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.11.0)
rubocop-ast (1.12.0)
parser (>= 3.0.1.1)
rubocop-discourse (2.4.2)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.4.0)
rubocop (~> 1.0)
rubocop-ast (>= 1.1.0)
rubocop-rspec (2.5.0)
rubocop (~> 1.19)
ruby-prof (1.4.3)
ruby-progressbar (1.11.0)
ruby-readability (0.7.0)
@ -454,8 +457,8 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (2.0.0)
unf_ext (0.0.8)
unicode-display_width (2.1.0)
unicorn (6.0.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@ -581,6 +584,7 @@ DEPENDENCIES
rspec
rspec-html-matchers
rspec-rails
rss
rswag-specs
rtlit
rubocop-discourse

View File

@ -15,7 +15,6 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://user-images.githubusercontent.com/1681963/52239245-04ad8280-289c-11e9-9c88-8c173d4a0422.png" width="720px"></a>
<a href="https://twittercommunity.com/"><img src="https://user-images.githubusercontent.com/1681963/52239250-04ad8280-289c-11e9-9e42-574f6eaab9d7.png" width="720px"></a>
<a href="https://discuss.atom.io/"><img src="https://user-images.githubusercontent.com/1681963/89088039-6735f080-d364-11ea-93a6-5629ea8738fe.png" width="720px"></a>
<a href="https://forums.gearboxsoftware.com/"><img src="https://user-images.githubusercontent.com/1681963/89088042-68ffb400-d364-11ea-93be-161ea04d8b29.png" width="720px"></a>

View File

@ -5,6 +5,7 @@ import loadScript from "discourse/lib/load-script";
import { makeArray } from "discourse-common/lib/helpers";
import { number } from "discourse/lib/formatter";
import { schedule } from "@ember/runloop";
import { bind } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["admin-report-chart"],
@ -12,23 +13,16 @@ export default Component.extend({
total: 0,
options: null,
init() {
this._super(...arguments);
this.resizeHandler = () =>
discourseDebounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
window.addEventListener("resize", this._resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
window.removeEventListener("resize", this._resizeHandler);
this._resetChart();
},
@ -179,4 +173,9 @@ export default Component.extend({
_applyChartGrouping(model, data, options) {
return Report.collapse(model, data, options.chartGrouping);
},
@bind
_resizeHandler() {
discourseDebounce(this, this._scheduleChartRendering, 500);
},
});

View File

@ -12,9 +12,20 @@ export default Component.extend(bufferedProperty("host"), {
editToggled: false,
tagName: "tr",
categoryId: null,
category: null,
editing: or("host.isNew", "editToggled"),
init() {
this._super(...arguments);
const host = this.host;
const categoryId = host.category_id || this.site.uncategorized_category_id;
const category = Category.findById(categoryId);
host.set("category", category);
},
@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || isEmpty(host);

View File

@ -17,7 +17,7 @@ import { schedule } from "@ember/runloop";
**/
export default Component.extend({
classNames: ["screened-ip-address-form"],
classNames: ["screened-ip-address-form", "inline-form"],
formSubmitted: false,
actionName: "block",

View File

@ -1,6 +0,0 @@
import ImageUploader from "discourse/components/image-uploader";
export default ImageUploader.extend({
layoutName: "components/image-uploader",
uploadUrlParams: "&for_site_setting=true",
});

View File

@ -3,70 +3,83 @@ import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isBlank } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { get } from "@ember/object";
import { action, get } from "@ember/object";
import { equal } from "@ember/object/computed";
import showModal from "discourse/lib/show-modal";
import { ajax } from "discourse/lib/ajax";
export default Controller.extend({
userModes: [
{ id: "all", name: I18n.t("admin.api.all_users") },
{ id: "single", name: I18n.t("admin.api.single_user") },
],
userModes: null,
useGlobalKey: false,
scopes: null,
@discourseComputed("userMode")
showUserSelector(mode) {
return mode === "single";
init() {
this._super(...arguments);
this.set("userModes", [
{ id: "all", name: I18n.t("admin.api.all_users") },
{ id: "single", name: I18n.t("admin.api.single_user") },
]);
this._loadScopes();
},
@discourseComputed("model.description", "model.username", "userMode")
saveDisabled(description, username, userMode) {
if (isBlank(description)) {
showUserSelector: equal("userMode", "single"),
@discourseComputed("model.{description,username}", "showUserSelector")
saveDisabled(model, showUserSelector) {
if (isBlank(model.description)) {
return true;
}
if (userMode === "single" && isBlank(username)) {
if (showUserSelector && isBlank(model.username)) {
return true;
}
return false;
},
actions: {
updateUsername(selected) {
this.set("model.username", get(selected, "firstObject"));
},
@action
updateUsername(selected) {
this.set("model.username", get(selected, "firstObject"));
},
changeUserMode(value) {
if (value === "all") {
this.model.set("username", null);
}
this.set("userMode", value);
},
@action
changeUserMode(userMode) {
if (userMode === "all") {
this.model.set("username", null);
}
this.set("userMode", userMode);
},
save() {
if (!this.useGlobalKey) {
const selectedScopes = Object.values(this.scopes)
.flat()
.filter((action) => {
return action.selected;
});
@action
save() {
if (!this.useGlobalKey) {
const selectedScopes = Object.values(this.scopes)
.flat()
.filterBy("selected");
this.model.set("scopes", selectedScopes);
}
this.model.set("scopes", selectedScopes);
}
this.model.save().catch(popupAjaxError);
},
return this.model.save().catch(popupAjaxError);
},
continue() {
this.transitionToRoute("adminApiKeys.show", this.model.id);
},
@action
continue() {
this.transitionToRoute("adminApiKeys.show", this.model.id);
},
showURLs(urls) {
return showModal("admin-api-key-urls", {
admin: true,
model: {
urls,
},
});
},
@action
showURLs(urls) {
return showModal("admin-api-key-urls", {
admin: true,
model: { urls },
});
},
_loadScopes() {
return ajax("/admin/api/keys/scopes.json")
.then((data) => {
this.set("scopes", data.scopes);
})
.catch(popupAjaxError);
},
});

View File

@ -146,7 +146,9 @@ export default Controller.extend({
@discourseComputed("model.translations")
translations(translations) {
return translations.map((setting) => ThemeSettings.create(setting));
return translations.map((setting) =>
ThemeSettings.create({ ...setting, textarea: true })
);
},
hasTranslations: notEmpty("translations"),

View File

@ -218,8 +218,15 @@ export default Controller.extend(CanCheckEmails, {
grantAdmin() {
return this.model
.grantAdmin()
.then(() => {
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
.then((result) => {
if (result.email_confirmation_required) {
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
} else {
const controller = showModal("grant-admin-second-factor", {
model: this.model,
});
controller.setResult(result);
}
})
.catch(popupAjaxError);
},

View File

@ -1,14 +1,18 @@
import { isNone } from "@ember/utils";
import { fmt, propertyNotEqual } from "discourse/lib/computed";
import { alias, oneWay } from "@ember/object/computed";
import I18n from "I18n";
import Mixin from "@ember/object/mixin";
import { Promise } from "rsvp";
import { ajax } from "discourse/lib/ajax";
import { categoryLinkHTML } from "discourse/helpers/category-link";
import discourseComputed from "discourse-common/utils/decorators";
import discourseComputed, { bind } from "discourse-common/utils/decorators";
import { htmlSafe } from "@ember/template";
import { on } from "@ember/object/evented";
import showModal from "discourse/lib/show-modal";
import { warn } from "@ember/debug";
import { action } from "@ember/object";
import { splitString } from "discourse/lib/utilities";
const CUSTOM_TYPES = [
"bool",
@ -32,26 +36,20 @@ const CUSTOM_TYPES = [
const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"];
function splitPipes(str) {
if (typeof str === "string") {
return str.split("|").filter(Boolean);
} else {
return [];
}
}
export default Mixin.create({
classNameBindings: [":row", ":setting", "overridden", "typeClass"],
content: alias("setting"),
validationMessage: null,
isSecret: oneWay("setting.secret"),
setting: null,
@discourseComputed("buffered.value", "setting.value")
dirty(bufferVal, settingVal) {
if (bufferVal === null || bufferVal === undefined) {
if (isNone(bufferVal)) {
bufferVal = "";
}
if (settingVal === null || settingVal === undefined) {
if (isNone(settingVal)) {
settingVal = "";
}
@ -61,21 +59,17 @@ export default Mixin.create({
@discourseComputed("setting", "buffered.value")
preview(setting, value) {
// A bit hacky, but allows us to use helpers
if (setting.get("setting") === "category_style") {
let category = this.site.get("categories.firstObject");
if (setting.setting === "category_style") {
const category = this.site.get("categories.firstObject");
if (category) {
return categoryLinkHTML(category, {
categoryStyle: value,
});
return categoryLinkHTML(category, { categoryStyle: value });
}
}
let preview = setting.get("preview");
const preview = setting.preview;
if (preview) {
return htmlSafe(
"<div class='preview'>" +
preview.replace(/\{\{value\}\}/g, value) +
"</div>"
);
const escapedValue = preview.replace(/\{\{value\}\}/g, value);
return htmlSafe(`<div class='preview'>${escapedValue}</div>`);
}
},
@ -103,51 +97,156 @@ export default Mixin.create({
return setting.type;
},
@discourseComputed("typeClass")
componentName(typeClass) {
return "site-settings/" + typeClass;
},
componentName: fmt("typeClass", "site-settings/%@"),
@discourseComputed("setting.anyValue")
allowAny(anyValue) {
return anyValue !== false;
},
@discourseComputed("setting.default", "buffered.value")
overridden(settingDefault, bufferedValue) {
return settingDefault !== bufferedValue;
},
overridden: propertyNotEqual("setting.default", "buffered.value"),
@discourseComputed("buffered.value")
bufferedValues: splitPipes,
bufferedValues(value) {
return splitString(value, "|");
},
@discourseComputed("setting.defaultValues")
defaultValues: splitPipes,
defaultValues(value) {
return splitString(value, "|");
},
@discourseComputed("defaultValues", "bufferedValues")
defaultIsAvailable(defaultValues, bufferedValues) {
return (
defaultValues &&
defaultValues.length > 0 &&
!defaultValues.every((value) => bufferedValues.includes(value))
);
},
_watchEnterKey: on("didInsertElement", function () {
$(this.element).on(
"keydown.setting-enter",
".input-setting-string",
(e) => {
if (e.key === "Enter") {
// enter key
this.send("save");
@action
update() {
const defaultUserPreferences = [
"default_email_digest_frequency",
"default_include_tl0_in_digests",
"default_email_level",
"default_email_messages_level",
"default_email_mailing_list_mode",
"default_email_mailing_list_mode_frequency",
"default_email_previous_replies",
"default_email_in_reply_to",
"default_other_new_topic_duration_minutes",
"default_other_auto_track_topics_after_msecs",
"default_other_notification_level_when_replying",
"default_other_external_links_in_new_tab",
"default_other_enable_quoting",
"default_other_enable_defer",
"default_other_dynamic_favicon",
"default_other_like_notification_frequency",
"default_other_skip_new_user_tips",
"default_topics_automatic_unpin",
"default_categories_watching",
"default_categories_tracking",
"default_categories_muted",
"default_categories_watching_first_post",
"default_categories_regular",
"default_tags_watching",
"default_tags_tracking",
"default_tags_muted",
"default_tags_watching_first_post",
"default_text_size",
"default_title_count_mode",
];
const key = this.buffered.get("setting");
if (defaultUserPreferences.includes(key)) {
const data = {};
data[key] = this.buffered.get("value");
ajax(`/admin/site_settings/${key}/user_count.json`, {
type: "PUT",
data,
}).then((result) => {
const count = result.user_count;
if (count > 0) {
const controller = showModal("site-setting-default-categories", {
model: { count, key: key.replaceAll("_", " ") },
admin: true,
});
controller.set("onClose", () => {
this.updateExistingUsers = controller.updateExistingUsers;
this.save();
});
} else {
this.save();
}
}
});
} else {
this.save();
}
},
@action
save() {
this._save()
.then(() => {
this.set("validationMessage", null);
this.commitBuffer();
if (AUTO_REFRESH_ON_SAVE.includes(this.setting.setting)) {
this.afterSave();
}
})
.catch((e) => {
if (e.jqXHR?.responseJSON?.errors) {
this.set("validationMessage", e.jqXHR.responseJSON.errors[0]);
} else {
this.set("validationMessage", I18n.t("generic_error"));
}
});
},
@action
cancel() {
this.rollbackBuffer();
},
@action
resetDefault() {
this.set("buffered.value", this.get("setting.default"));
},
@action
toggleSecret() {
this.toggleProperty("isSecret");
},
@action
setDefaultValues() {
this.set(
"buffered.value",
this.bufferedValues.concat(this.defaultValues).uniq().join("|")
);
return false;
},
@bind
_handleKeydown(event) {
if (
event.key === "Enter" &&
event.target.classList.contains("input-setting-string")
) {
this.save();
}
},
_watchEnterKey: on("didInsertElement", function () {
this.element.addEventListener("keydown", this._handleKeydown);
}),
_removeBindings: on("willDestroyElement", function () {
$(this.element).off("keydown.setting-enter");
this.element.removeEventListener("keydown", this._handleKeydown);
}),
_save() {
@ -156,110 +255,4 @@ export default Mixin.create({
});
return Promise.resolve();
},
actions: {
update() {
const defaultUserPreferences = [
"default_email_digest_frequency",
"default_include_tl0_in_digests",
"default_email_level",
"default_email_messages_level",
"default_email_mailing_list_mode",
"default_email_mailing_list_mode_frequency",
"default_email_previous_replies",
"default_email_in_reply_to",
"default_other_new_topic_duration_minutes",
"default_other_auto_track_topics_after_msecs",
"default_other_notification_level_when_replying",
"default_other_external_links_in_new_tab",
"default_other_enable_quoting",
"default_other_enable_defer",
"default_other_dynamic_favicon",
"default_other_like_notification_frequency",
"default_other_skip_new_user_tips",
"default_topics_automatic_unpin",
"default_categories_watching",
"default_categories_tracking",
"default_categories_muted",
"default_categories_watching_first_post",
"default_categories_regular",
"default_tags_watching",
"default_tags_tracking",
"default_tags_muted",
"default_tags_watching_first_post",
"default_text_size",
"default_title_count_mode",
];
const key = this.buffered.get("setting");
if (defaultUserPreferences.includes(key)) {
const data = {};
data[key] = this.buffered.get("value");
ajax(`/admin/site_settings/${key}/user_count.json`, {
type: "PUT",
data,
}).then((result) => {
const count = result.user_count;
if (count > 0) {
const controller = showModal("site-setting-default-categories", {
model: {
count: result.user_count,
key: key.replace(/_/g, " "),
},
admin: true,
});
controller.set("onClose", () => {
this.updateExistingUsers = controller.updateExistingUsers;
this.send("save");
});
} else {
this.send("save");
}
});
} else {
this.send("save");
}
},
save() {
this._save()
.then(() => {
this.set("validationMessage", null);
this.commitBuffer();
if (AUTO_REFRESH_ON_SAVE.includes(this.setting.setting)) {
this.afterSave();
}
})
.catch((e) => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
this.set("validationMessage", e.jqXHR.responseJSON.errors[0]);
} else {
this.set("validationMessage", I18n.t("generic_error"));
}
});
},
cancel() {
this.rollbackBuffer();
},
resetDefault() {
this.set("buffered.value", this.get("setting.default"));
},
toggleSecret() {
this.toggleProperty("isSecret");
},
setDefaultValues() {
this.set(
"buffered.value",
this.bufferedValues.concat(this.defaultValues).uniq().join("|")
);
return false;
},
},
});

View File

@ -99,9 +99,20 @@ const AdminUser = User.extend({
});
},
grantAdmin() {
grantAdmin(data) {
return ajax(`/admin/users/${this.id}/grant_admin`, {
type: "PUT",
data,
}).then((resp) => {
if (resp.success && !resp.email_confirmation_required) {
this.setProperties({
admin: true,
can_grant_admin: false,
can_revoke_admin: true,
});
}
return resp;
});
},

View File

@ -1,17 +1,7 @@
import Route from "@ember/routing/route";
import { ajax } from "discourse/lib/ajax";
export default Route.extend({
model() {
return this.store.createRecord("api-key");
},
setupController(controller, model) {
ajax("/admin/api/keys/scopes.json").then((data) => {
controller.setProperties({
scopes: data.scopes,
model,
});
});
},
});

View File

@ -20,6 +20,8 @@ export default DiscourseRoute.extend({
originalPrimaryGroupId: model.primary_group_id,
availableGroups: this._availableGroups,
customGroupIdsBuffer: model.customGroups.mapBy("id"),
ssoExternalEmail: null,
ssoLastPayload: null,
model,
});
},

View File

@ -1,6 +1,6 @@
{{#link-to "adminApiKeys.index" class="go-back"}}
{{d-icon "arrow-left"}}
{{i18n "admin.api.all_api_keys"}}
<span>{{i18n "admin.api.all_api_keys"}}</span>
{{/link-to}}
<div class="api-key api-key-new">

View File

@ -4,7 +4,7 @@
{{#if model}}
<form class="form-horizontal">
<div class="badge-preview">
<div class="badge-preview control-group">
{{#if model}}
{{icon-or-image model}}
<span class="badge-display-name">{{model.name}}</span>
@ -12,11 +12,11 @@
<span class="badge-placeholder">{{i18n "admin.badges.mass_award.no_badge_selected"}}</span>
{{/if}}
</div>
<div>
<div class="control-group">
<h4>{{i18n "admin.badges.mass_award.upload_csv"}}</h4>
<input type="file" id="massAwardCSVUpload" accept=".csv" onchange={{action "updateFileSelected"}}>
</div>
<div>
<div class="control-group">
<label>
{{input type="checkbox" checked=replaceBadgeOwners}}
{{i18n "admin.badges.mass_award.replace_owners"}}

View File

@ -1,6 +1,6 @@
{{#d-section class="current-badge content-body"}}
<form class="form-horizontal">
<div>
<div class="control-group">
<label for="name">{{i18n "admin.badges.name"}}</label>
{{#if readOnly}}
{{input type="text" name="name" value=buffered.name disabled=true}}
@ -14,9 +14,9 @@
{{/if}}
</div>
<div>
<div class="control-group">
<label for="graphic">{{i18n "admin.badges.graphic"}}</label>
<div class="radios">
<div class="radios inline-form full-width">
<label class="radio-label" for="badge-icon">
{{radio-button
name="badge-icon"
@ -40,12 +40,14 @@
</label>
</div>
{{#if imageUploaderSelected}}
{{image-uploader
{{uppy-image-uploader
id="badge-image-uploader"
imageUrl=buffered.image_url
type="badge_image"
onUploadDone=(action "setImage")
onUploadDeleted=(action "removeImage")
type="badge_image"
class="no-repeat contain-image"}}
class="no-repeat contain-image"
}}
<div class="control-instructions">
<p class="help">{{i18n "admin.badges.image_help"}}</p>
</div>
@ -59,7 +61,7 @@
{{/if}}
</div>
<div>
<div class="control-group">
<label for="badge_type_id">{{i18n "admin.badges.badge_type"}}</label>
{{combo-box
name="badge_type_id"
@ -70,7 +72,7 @@
}}
</div>
<div>
<div class="control-group">
<label for="badge_grouping_id">{{i18n "admin.badges.badge_grouping"}}</label>
<div class="badge-grouping-control">
@ -90,7 +92,7 @@
</div>
</div>
<div>
<div class="control-group">
<label for="description">{{i18n "admin.badges.description"}}</label>
{{#if buffered.system}}
{{textarea name="description" value=buffered.description disabled=true}}
@ -104,7 +106,7 @@
{{/if}}
</div>
<div>
<div class="control-group">
<label for="long_description">{{i18n "admin.badges.long_description"}}</label>
{{#if buffered.system}}
{{textarea name="long_description" value=buffered.long_description disabled=true}}
@ -119,7 +121,7 @@
</div>
{{#if siteSettings.enable_badge_sql}}
<div>
<div class="control-group">
<label for="query">{{i18n "admin.badges.query"}}</label>
{{ace-editor content=buffered.query mode="sql" disabled=readOnly}}
</div>
@ -132,21 +134,21 @@
{{i18n "loading"}}
{{/if}}
<div>
<div class="control-group">
<label>
{{input type="checkbox" checked=buffered.auto_revoke disabled=readOnly}}
{{i18n "admin.badges.auto_revoke"}}
</label>
</div>
<div>
<div class="control-group">
<label>
{{input type="checkbox" checked=buffered.target_posts disabled=readOnly}}
{{i18n "admin.badges.target_posts"}}
</label>
</div>
<div>
<div class="control-group">
<label for="trigger">{{i18n "admin.badges.trigger"}}</label>
{{combo-box
name="trigger"
@ -159,39 +161,41 @@
{{/if}}
{{/if}}
<div>
<label>
{{input type="checkbox" checked=buffered.allow_title}}
{{i18n "admin.badges.allow_title"}}
</label>
</div>
<div class="control-group">
<div>
<label>
{{input type="checkbox" checked=buffered.allow_title}}
{{i18n "admin.badges.allow_title"}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.multiple_grant disabled=readOnly}}
{{i18n "admin.badges.multiple_grant"}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.multiple_grant disabled=readOnly}}
{{i18n "admin.badges.multiple_grant"}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.listable disabled=readOnly}}
{{i18n "admin.badges.listable"}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.listable disabled=readOnly}}
{{i18n "admin.badges.listable"}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.show_posts disabled=readOnly}}
{{i18n "admin.badges.show_posts"}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.show_posts disabled=readOnly}}
{{i18n "admin.badges.show_posts"}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.enabled}}
{{i18n "admin.badges.enabled"}}
</label>
<div>
<label>
{{input type="checkbox" checked=buffered.enabled}}
{{i18n "admin.badges.enabled"}}
</label>
</div>
</div>
<div class="buttons">

View File

@ -27,7 +27,7 @@
<td><div class="label">{{i18n "admin.embedding.host"}}</div>{{host.host}}</td>
<td><div class="label">{{i18n "admin.embedding.class_name"}}</div>{{host.class_name}}</td>
<td><div class="label">{{i18n "admin.embedding.allowed_paths"}}</div>{{host.allowed_paths}}</td>
<td><div class="label">{{i18n "admin.embedding.category"}}</div>{{category-badge host.category}}</td>
<td><div class="label">{{i18n "admin.embedding.category"}}</div>{{category-badge host.category allowUncategorized=true}}</td>
<td class="controls">
{{d-button icon="pencil-alt" action=(action "edit")}}
{{d-button icon="far-trash-alt" action=(action "delete") class="btn-danger"}}

View File

@ -1,30 +1,35 @@
<b>{{i18n "admin.permalink.form.label"}}</b>
<div class="inline-form">
<label>{{i18n "admin.permalink.form.label"}}</label>
{{text-field
value=url
disabled=formSubmitted
class="permalink-url"
placeholderKey="admin.permalink.url"
autocorrect="off"
autocapitalize="off"}}
{{text-field
value=url
disabled=formSubmitted
class="permalink-url"
placeholderKey="admin.permalink.url"
autocorrect="off"
autocapitalize="off"
}}
{{combo-box
content=permalinkTypes
value=permalinkType
onChange=(action (mut permalinkType))
class="permalink-type"
}}
{{combo-box
content=permalinkTypes
value=permalinkType
onChange=(action (mut permalinkType))
class="permalink-type"
}}
{{text-field
value=permalink_type_value
disabled=formSubmitted
class="external-url"
placeholderKey=permalinkTypePlaceholder
autocorrect="off"
autocapitalize="off"}}
{{text-field
value=permalink_type_value
disabled=formSubmitted
class="external-url"
placeholderKey=permalinkTypePlaceholder
autocorrect="off"
autocapitalize="off"
}}
{{d-button
class="btn-default"
action=(action "submit")
disabled=formSubmitted
label="admin.permalink.form.add"}}
{{d-button
class="btn-default"
action=(action "submit")
disabled=formSubmitted
label="admin.permalink.form.add"
}}
</div>

View File

@ -1,4 +1,4 @@
<b>{{i18n "admin.logs.screened_ips.form.label"}}</b>
<label>{{i18n "admin.logs.screened_ips.form.label"}}</label>
{{text-field value=ip_address disabled=formSubmitted class="ip-address-input" placeholderKey="admin.logs.screened_ips.form.ip_address" autocorrect="off" autocapitalize="off"}}
{{combo-box

View File

@ -1,2 +0,0 @@
{{text-field value=(html-safe value) classNames="input-setting-string"}}
<div class="desc">{{html-safe setting.description}}</div>

View File

@ -1,2 +1,8 @@
{{site-settings-image-uploader imageUrl=value placeholderUrl=setting.placeholder type="site_setting"}}
{{uppy-image-uploader
imageUrl=value
placeholderUrl=setting.placeholder
additionalParams=(hash for_site_setting=true)
type="site_setting"
id=(concat "site-setting-image-uploader-" setting.setting)
}}
<div class="desc">{{html-safe setting.description}}</div>

View File

@ -17,7 +17,7 @@
label="admin.customize.copy"
}}
{{d-button
class="btn-default"
class="btn-default copy-to-clipboard"
action=(action "copyToClipboard" model)
icon="far-clipboard"
label="admin.customize.copy_to_clipboard"
@ -38,8 +38,6 @@
{{/if}}
</div>
<br>
<div class="admin-controls">
{{#unless model.theme_id}}
<div class="pull-right">

View File

@ -16,21 +16,24 @@
<form>
<div class="admin-controls">
{{#if sendingEmail}}
<div class="controls">{{i18n "admin.email.sending_test"}}</div>
{{else}}
<div class="controls">
{{text-field value=testEmailAddress placeholderKey="admin.email.test_email_address"}}
<div class="controls">
<div class="inline-form">
{{#if sendingEmail}}
{{i18n "admin.email.sending_test"}}
{{else}}
{{text-field value=testEmailAddress placeholderKey="admin.email.test_email_address"}}
{{d-button
class="btn-primary"
action=(action "sendTestEmail")
disabled=sendTestEmailDisabled
label="admin.email.send_test"
type="submit"
}}
{{#if sentTestEmailMessage}}
<span class="result-message">{{sentTestEmailMessage}}</span>
{{/if}}
{{/if}}
</div>
<div class="controls">
{{d-button
class="btn-primary"
action=(action "sendTestEmail")
disabled=sendTestEmailDisabled
label="admin.email.send_test"
type="submit"}}
{{#if sentTestEmailMessage}}<span class="result-message">{{sentTestEmailMessage}}</span>{{/if}}
</div>
{{/if}}
</div>
</div>
</form>

View File

@ -2,32 +2,35 @@
<div class="admin-controls email-preview">
<div class="controls">
<label for="last-seen">{{i18n "admin.email.last_seen_user"}}</label>
{{date-picker-past value=lastSeen id="last-seen"}}
<label>{{i18n "admin.email.user"}}:</label>
{{email-group-user-chooser
value=username
onChange=(action "updateUsername")
options=(hash
maximum=1
)
}}
{{d-button
class="btn-primary digest-refresh-button"
action=(action "refresh")
label="admin.email.refresh"}}
<div class="toggle">
<label>{{i18n "admin.email.format"}}</label>
{{#if showHtml}}
<span>{{i18n "admin.email.html"}}</span>
|
<a href {{action "toggleShowHtml"}}>
{{i18n "admin.email.text"}}
</a>
{{else}}
<a href {{action "toggleShowHtml"}}>{{i18n "admin.email.html"}}</a> |
<span>{{i18n "admin.email.text"}}</span>
{{/if}}
<div class="inline-form">
<label for="last-seen">{{i18n "admin.email.last_seen_user"}}</label>
{{date-picker-past value=lastSeen id="last-seen"}}
<label>{{i18n "admin.email.user"}}:</label>
{{email-group-user-chooser
value=username
onChange=(action "updateUsername")
options=(hash
maximum=1
)
}}
{{d-button
class="btn-primary digest-refresh-button"
action=(action "refresh")
label="admin.email.refresh"
}}
<div class="toggle">
<label>{{i18n "admin.email.format"}}</label>
{{#if showHtml}}
<span>{{i18n "admin.email.html"}}</span>
|
<a href {{action "toggleShowHtml"}}>
{{i18n "admin.email.text"}}
</a>
{{else}}
<a href {{action "toggleShowHtml"}}>{{i18n "admin.email.html"}}</a> |
<span>{{i18n "admin.email.text"}}</span>
{{/if}}
</div>
</div>
</div>
</div>
@ -37,20 +40,22 @@
<div class="email-preview-digest">
{{#if showSendEmailForm}}
<div class="controls">
{{#if sendingEmail}}
{{i18n "admin.email.sending_test"}}
{{else}}
<label>{{i18n "admin.email.send_digest_label"}}</label>
{{text-field value=email placeholderKey="admin.email.test_email_address"}}
{{d-button
class="btn-default"
action=(action "sendEmail")
disabled=sendEmailDisabled
label="admin.email.send_digest"}}
{{#if sentEmail}}
<span class="result-message">{{i18n "admin.email.sent_test"}}</span>
<div class="inline-form">
{{#if sendingEmail}}
{{i18n "admin.email.sending_test"}}
{{else}}
<label>{{i18n "admin.email.send_digest_label"}}</label>
{{text-field value=email placeholderKey="admin.email.test_email_address"}}
{{d-button
class="btn-default"
action=(action "sendEmail")
disabled=sendEmailDisabled
label="admin.email.send_digest"}}
{{#if sentEmail}}
<span class="result-message">{{i18n "admin.email.sent_test"}}</span>
{{/if}}
{{/if}}
{{/if}}
</div>
</div>
{{/if}}

View File

@ -16,7 +16,7 @@
<tr>
<th>{{i18n "admin.emoji.image"}}</th>
<th>{{i18n "admin.emoji.name"}}</th>
<th>
<th colspan="2">
{{combo-box
value=filter
content=sortingGroups
@ -25,22 +25,21 @@
onChange=(action "filterGroups")
}}
</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each sortedEmojis as |e|}}
<tr>
<th><img class="emoji emoji-custom" src={{e.url}} title={{e.name}} alt={{i18n "admin.emoji.alt"}}></th>
<th>:{{e.name}}:</th>
<th>{{e.group}}</th>
<th>
<td><img class="emoji emoji-custom" src={{e.url}} title={{e.name}} alt={{i18n "admin.emoji.alt"}}></td>
<td>:{{e.name}}:</td>
<td>{{e.group}}</td>
<td class="action">
{{d-button
action=(action "destroyEmoji" e)
class="btn-danger"
icon="far-trash-alt"
}}
</th>
</td>
</tr>
{{/each}}
</tbody>

View File

@ -1,7 +1,7 @@
<p>{{i18n "admin.logs.screened_ips.description"}}</p>
<div class="screened-ip-controls">
<div class="filter-screened-ip-address">
<div class="filter-screened-ip-address inline-form">
{{text-field
value=filter
class="ip-address-input"
@ -82,7 +82,9 @@
action=(action "save")
actionParam=item
label="admin.logs.save"}}
<a href {{action "cancel" item}}>{{i18n "cancel"}}</a>
<a href {{action "cancel" item}} class="cancel-action">
{{i18n "cancel"}}
</a>
{{else}}
{{d-button
class="btn-default btn-danger"

View File

@ -1,29 +1,29 @@
<div class="staff-action-logs-controls">
{{#if filtersExists}}
<div class="staff-action-logs-filters">
<a href {{action "clearAllFilters"}} class="clear-filters filter">
<a href {{action "clearAllFilters"}} class="clear-filters filter btn">
<span class="label">{{i18n "admin.logs.staff_actions.clear_filters"}}</span>
</a>
{{#if actionFilter}}
<a href {{action "clearFilter" "actionFilter"}} class="filter">
<a href {{action "clearFilter" "actionFilter"}} class="filter btn">
<span class="label">{{i18n "admin.logs.action"}}</span>: {{actionFilter}}
{{d-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.acting_user}}
<a href {{action "clearFilter" "acting_user"}} class="filter">
<a href {{action "clearFilter" "acting_user"}} class="filter btn">
<span class="label">{{i18n "admin.logs.staff_actions.staff_user"}}</span>: {{filters.acting_user}}
{{d-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.target_user}}
<a href {{action "clearFilter" "target_user"}} class="filter">
<a href {{action "clearFilter" "target_user"}} class="filter btn">
<span class="label">{{i18n "admin.logs.staff_actions.target_user"}}</span>: {{filters.target_user}}
{{d-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.subject}}
<a href {{action "clearFilter" "subject"}} class="filter">
<a href {{action "clearFilter" "subject"}} class="filter btn">
<span class="label">{{i18n "admin.logs.staff_actions.subject"}}</span>: {{filters.subject}}
{{d-icon "times-circle"}}
</a>

View File

@ -1,9 +1,11 @@
<div class="admin-controls">
<div class="controls">
{{d-button action=(action "toggleMenu") class="menu-toggle" icon="bars"}}
{{text-field id="setting-filter" value=filter placeholderKey="type_to_filter" class="no-blur"}}
{{d-button class="btn-default" id="clear-filter" action=(action "clearFilter") label="admin.site_settings.clear_filter"}}
<div class="inline-form">
{{d-button action=(action "toggleMenu") class="menu-toggle" icon="bars"}}
{{text-field id="setting-filter" value=filter placeholderKey="type_to_filter" class="no-blur"}}
{{d-button class="btn-default" id="clear-filter" action=(action "clearFilter") label="admin.site_settings.clear_filter"}}
</div>
</div>
<div class="search controls">
<label>

View File

@ -14,7 +14,7 @@
<p>{{i18n "admin.badges.no_badges"}}</p>
{{else}}
<form class="form-horizontal">
<div>
<div class="control-group">
<label>{{i18n "admin.badges.badge"}}</label>
{{combo-box
filterable=true
@ -23,7 +23,7 @@
onChange=(action (mut selectedBadgeId))
}}
</div>
<div>
<div class="control-group">
<label>{{i18n "admin.badges.reason"}}</label>
{{input type="text" value=badgeReason}}<br><small>{{i18n "admin.badges.reason_help"}}</small>
</div>

View File

@ -334,7 +334,7 @@
{{/if}}
{{#if model.can_grant_admin}}
{{d-button
class="btn-default"
class="btn-default grant-admin"
action=(action "grantAdmin")
icon="shield-alt"
label="admin.user.grant_admin"}}

View File

@ -1,9 +1,11 @@
<div class="admin-contents">
<div class="admin-controls">
<div class="controls">
{{d-button action=(action "toggleMenu") class="menu-toggle" icon="bars"}}
{{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}}
{{d-button action=(action "clearFilter") label="admin.watched_words.clear_filter"}}
<div class="inline-form">
{{d-button action=(action "toggleMenu") class="menu-toggle" icon="bars"}}
{{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}}
{{d-button action=(action "clearFilter") label="admin.watched_words.clear_filter"}}
</div>
</div>
</div>

View File

@ -6,13 +6,13 @@
<div class="web-hook-container">
<p>{{i18n "admin.web_hooks.detailed_instruction"}}</p>
<form class="web-hook form-horizontal">
<div>
<div class="control-group">
<label for="payload-url">{{i18n "admin.web_hooks.payload_url"}}</label>
{{text-field name="payload-url" value=model.payload_url placeholderKey="admin.web_hooks.payload_url_placeholder"}}
{{input-tip validation=urlValidation}}
</div>
<div>
<div class="control-group">
<label for="content-type">{{i18n "admin.web_hooks.content_type"}}</label>
{{combo-box
content=contentTypes
@ -22,13 +22,13 @@
}}
</div>
<div>
<div class="control-group">
<label for="secret">{{i18n "admin.web_hooks.secret"}}</label>
{{text-field name="secret" value=model.secret placeholderKey="admin.web_hooks.secret_placeholder"}}
{{input-tip validation=secretValidation}}
</div>
<div class="cbox10">
<div class="control-group">
<label>{{i18n "admin.web_hooks.event_chooser"}}</label>
<div>
{{radio-button class="subscription-choice" name="subscription-choice" value="individual" selection=model.webHookType}}
@ -48,7 +48,7 @@
</div>
</div>
<div class="filters">
<div class="filters control-group">
<div class="filter">
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.categories_filter"}}</label>
{{category-selector

View File

@ -31,6 +31,7 @@
//= require ./discourse/app/lib/text-direction
//= require ./discourse/app/lib/eyeline
//= require ./discourse/app/lib/show-modal
//= require ./discourse/app/lib/download-calendar
//= require ./discourse/app/mixins/scrolling
//= require ./discourse/app/lib/ajax-error
//= require ./discourse/app/models/result-set

View File

@ -1,4 +1,4 @@
if (!window.WeakMap || !window.Promise) {
if (!window.WeakMap || !window.Promise || typeof globalThis === "undefined") {
window.unsupportedBrowser = true;
} else {
// Some implementations of `WeakMap.prototype.has` do not accept false

View File

@ -2,11 +2,6 @@ define("message-bus-client", ["exports"], function (__exports__) {
__exports__.default = window.MessageBus;
});
define("mousetrap-global-bind", ["exports"], function (__exports__) {
// In the Rails app it's applied from the vendored file
__exports__.default = {};
});
define("ember-buffered-proxy/proxy", ["exports"], function (__exports__) {
__exports__.default = window.BufferedProxy;
});
@ -19,8 +14,8 @@ define("xss", ["exports"], function (__exports__) {
__exports__.default = window.filterXSS;
});
define("mousetrap", ["exports"], function (__exports__) {
__exports__.default = window.Mousetrap;
define("@discourse/itsatrap", ["exports"], function (__exports__) {
__exports__.default = window.ItsATrap;
});
define("@popperjs/core", ["exports"], function (__exports__) {

View File

@ -1,5 +1,4 @@
import Application from "@ember/application";
import Mousetrap from "mousetrap";
import { buildResolver } from "discourse-common/resolver";
import { isTesting } from "discourse-common/config/environment";
@ -13,11 +12,6 @@ const Discourse = Application.extend({
paste: "paste",
},
reset() {
this._super(...arguments);
Mousetrap.reset();
},
Resolver: buildResolver("discourse"),
_prepareInitializer(moduleName) {

View File

@ -11,7 +11,7 @@ import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
import Component from "@ember/component";
import I18n from "I18n";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import Mousetrap from "mousetrap";
import ItsATrap from "@discourse/itsatrap";
import { Promise } from "rsvp";
import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
import { action } from "@ember/object";
@ -37,6 +37,7 @@ export default Component.extend({
_savingBookmarkManually: null,
_saving: null,
_deleting: null,
_itsatrap: null,
postDetectedLocalDate: null,
postDetectedLocalTime: null,
postDetectedLocalTimezone: null,
@ -44,7 +45,6 @@ export default Component.extend({
userTimezone: null,
showOptions: null,
model: null,
afterSave: null,
@on("init")
@ -62,6 +62,7 @@ export default Component.extend({
prefilledDatetime: null,
userTimezone: this.currentUser.resolvedTimezone(this.currentUser),
showOptions: false,
_itsatrap: new ItsATrap(),
});
this.registerOnCloseHandler(this._onModalClose.bind(this));
@ -123,9 +124,8 @@ export default Component.extend({
_bindKeyboardShortcuts() {
KeyboardShortcuts.pause();
this._mousetrap = new Mousetrap();
Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => {
this._mousetrap.bind(shortcut, () => {
this._itsatrap.bind(shortcut, () => {
let binding = BOOKMARK_BINDINGS[shortcut];
this.send(binding.handler);
return false;
@ -167,25 +167,13 @@ export default Component.extend({
localStorage.bookmarkDeleteOption = this.autoDeletePreference;
let reminderType;
if (this.selectedReminderType === TIME_SHORTCUT_TYPES.NONE) {
reminderType = null;
} else if (
this.selectedReminderType === TIME_SHORTCUT_TYPES.LAST_CUSTOM ||
this.selectedReminderType === TIME_SHORTCUT_TYPES.POST_LOCAL_DATE
) {
reminderType = TIME_SHORTCUT_TYPES.CUSTOM;
} else {
reminderType = this.selectedReminderType;
}
const data = {
reminder_type: reminderType,
reminder_at: reminderAtISO,
name: this.model.name,
post_id: this.model.postId,
id: this.model.id,
auto_delete_preference: this.autoDeletePreference,
for_topic: this.model.forTopic,
};
if (this.editingExistingBookmark) {
@ -207,9 +195,10 @@ export default Component.extend({
return;
}
this.afterSave({
reminderAt: reminderAtISO,
reminderType: this.selectedReminderType,
autoDeletePreference: this.autoDeletePreference,
reminder_at: reminderAtISO,
for_topic: this.model.forTopic,
auto_delete_preference: this.autoDeletePreference,
post_id: this.model.postId,
id: this.model.id || response.id,
name: this.model.name,
});
@ -220,7 +209,7 @@ export default Component.extend({
type: "DELETE",
}).then((response) => {
if (this.afterDelete) {
this.afterDelete(response.topic_bookmarked);
this.afterDelete(response.topic_bookmarked, this.model.id);
}
});
},
@ -266,7 +255,9 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
this._mousetrap.reset();
this._itsatrap?.destroy();
this.set("_itsatrap", null);
KeyboardShortcuts.unpause();
},
@ -274,7 +265,7 @@ export default Component.extend({
showDelete: notEmpty("model.id"),
userHasTimezoneSet: notEmpty("userTimezone"),
editingExistingBookmark: and("model", "model.id"),
existingBookmarkHasReminder: and("model", "model.reminderAt"),
existingBookmarkHasReminder: and("model", "model.id", "model.reminderAt"),
@discourseComputed("postDetectedLocalDate", "postDetectedLocalTime")
showPostLocalDate(postDetectedLocalDate, postDetectedLocalTime) {
@ -320,6 +311,32 @@ export default Component.extend({
return customOptions;
},
@discourseComputed("existingBookmarkHasReminder")
customTimeShortcutLabels(existingBookmarkHasReminder) {
const labels = {};
if (existingBookmarkHasReminder) {
labels[TIME_SHORTCUT_TYPES.NONE] =
"bookmarks.remove_reminder_keep_bookmark";
}
return labels;
},
@discourseComputed("editingExistingBookmark", "existingBookmarkHasReminder")
hiddenTimeShortcutOptions(
editingExistingBookmark,
existingBookmarkHasReminder
) {
if (!editingExistingBookmark) {
return [];
}
if (!existingBookmarkHasReminder) {
return [TIME_SHORTCUT_TYPES.NONE];
}
return [];
},
@discourseComputed()
additionalTimeShortcutOptions() {
let additional = [];

View File

@ -1,39 +1,4 @@
import Component from "@ember/component";
import { schedule } from "@ember/runloop";
import { reads } from "@ember/object/computed";
import showModal from "discourse/lib/show-modal";
export default Component.extend({
classNames: ["bulk-select-container"],
didInsertElement() {
this._super(...arguments);
schedule("afterRender", () => {
let headerHeight =
document.querySelector(".d-header-wrap").offsetHeight || 0;
document.querySelector(".bulk-select-container").style.top =
headerHeight + 20 + "px";
});
},
canDoBulkActions: reads("currentUser.staff"),
actions: {
showBulkActions() {
const controller = showModal("topic-bulk-actions", {
model: {
topics: this.selected,
category: this.category,
},
title: "topics.bulk.actions",
});
const action = this.action;
if (action) {
controller.set("refreshClosure", () => action());
}
},
},
});
// TODO: Remove in December 2021
export default Component.extend({});

View File

@ -24,8 +24,15 @@ export default Component.extend({
options: alias("model.replyOptions"),
action: alias("model.action"),
@discourseComputed("options", "action")
// Note we update when some other attributes like tag/category change to allow
// text customizations to use those.
@discourseComputed("options", "action", "model.tags", "model.category")
actionTitle(opts, action) {
let result = this.model.customizationFor("actionTitle");
if (result) {
return result;
}
if (TITLES[action]) {
return I18n.t(TITLES[action]);
}

View File

@ -15,7 +15,6 @@ const START_EVENTS = "touchstart mousedown";
const DRAG_EVENTS = "touchmove mousemove";
const END_EVENTS = "touchend mouseup";
const MIN_COMPOSER_SIZE = 240;
const THROTTLE_RATE = 20;
function mouseYPos(e) {
@ -121,7 +120,6 @@ export default Component.extend(KeyEnterEscape, {
const winHeight = $(window).height();
size = Math.min(size, winHeight - headerHeight());
size = Math.max(size, MIN_COMPOSER_SIZE);
this.movePanels(size);
$composer.height(size);
};

View File

@ -3,6 +3,7 @@ import {
authorizesAllExtensions,
authorizesOneOrMoreImageExtensions,
} from "discourse/lib/uploads";
import { BasePlugin } from "@uppy/core";
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
import {
caretPosition,
@ -61,6 +62,23 @@ export function cleanUpComposerUploadProcessor() {
uploadProcessorActions = {};
}
let uploadPreProcessors = [];
export function addComposerUploadPreProcessor(pluginClass, optionsResolverFn) {
if (!(pluginClass.prototype instanceof BasePlugin)) {
throw new Error(
"Composer upload preprocessors must inherit from the Uppy BasePlugin class."
);
}
uploadPreProcessors.push({
pluginClass,
optionsResolverFn,
});
}
export function cleanUpComposerUploadPreProcessor() {
uploadPreProcessors = [];
}
let uploadMarkdownResolvers = [];
export function addComposerUploadMarkdownResolver(resolver) {
uploadMarkdownResolvers.push(resolver);
@ -72,6 +90,8 @@ export function cleanUpComposerUploadMarkdownResolver() {
export default Component.extend(ComposerUpload, {
classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"],
fileUploadElementId: "file-uploader",
mobileFileUploaderId: "mobile-file-upload",
shouldBuildScrollMap: true,
scrollMap: null,
processPreview: true,
@ -79,6 +99,7 @@ export default Component.extend(ComposerUpload, {
uploadMarkdownResolvers,
uploadProcessorActions,
uploadProcessorQueue,
uploadPreProcessors,
uploadHandlers,
@discourseComputed("composer.requiredCategoryMissing")
@ -103,11 +124,6 @@ export default Component.extend(ComposerUpload, {
);
},
@discourseComputed("composer.requiredCategoryMissing", "composer.replyLength")
disableTextarea(requiredCategoryMissing, replyLength) {
return requiredCategoryMissing && replyLength === 0;
},
@observes("focusTarget")
setFocus() {
if (this.focusTarget === "editor") {
@ -218,8 +234,10 @@ export default Component.extend(ComposerUpload, {
putCursorAtEnd(this.element.querySelector(".d-editor-input"));
}
this._bindUploadTarget();
this._bindMobileUploadButton();
if (this.allowUpload) {
this._bindUploadTarget();
this._bindMobileUploadButton();
}
this.appEvents.trigger("composer:will-open");
},

View File

@ -1,6 +1,10 @@
import Button from "discourse/components/d-button";
import I18n from "I18n";
import { translateModKey } from "discourse/lib/utilities";
export default Button.extend({
classNameBindings: [":btn-primary", ":create", "disableSubmit:disabled"],
title: "composer.title",
translatedTitle: I18n.t("composer.title", {
modifier: translateModKey("Meta+"),
}),
});

View File

@ -40,4 +40,12 @@ export default Component.extend({
? "discourse-compress"
: "discourse-expand";
},
@discourseComputed("disableTextarea")
showFullScreenButton(disableTextarea) {
if (this.site.mobileView) {
return false;
}
return !disableTextarea;
},
});

View File

@ -85,15 +85,14 @@ export default Component.extend({
return translatedLabel;
},
@discourseComputed("ariaLabel", "translatedAriaLabel", "computedLabel")
computedAriaLabel(ariaLabel, translatedAriaLabel, computedLabel) {
@discourseComputed("ariaLabel", "translatedAriaLabel")
computedAriaLabel(ariaLabel, translatedAriaLabel) {
if (ariaLabel) {
return I18n.t(ariaLabel);
}
if (translatedAriaLabel) {
return translatedAriaLabel;
}
return computedLabel;
},
@discourseComputed("ariaExpanded")

View File

@ -6,6 +6,11 @@ import logout from "discourse/lib/logout";
import { inject as service } from "@ember/service";
import { setLogoffCallback } from "discourse/lib/ajax";
let pluginCounterFunctions = [];
export function addPluginDocumentTitleCounter(counterFunction) {
pluginCounterFunctions.push(counterFunction);
}
export default Component.extend({
tagName: "",
documentTitle: service(),
@ -44,10 +49,11 @@ export default Component.extend({
return;
}
this.documentTitle.updateNotificationCount(
const count =
pluginCounterFunctions.reduce((sum, fn) => sum + fn(), 0) +
this.currentUser.unread_notifications +
this.currentUser.unread_high_priority_notifications
);
this.currentUser.unread_high_priority_notifications;
this.documentTitle.updateNotificationCount(count);
},
@bind

View File

@ -1,5 +1,9 @@
import { ajax } from "discourse/lib/ajax";
import { caretPosition, inCodeBlock } from "discourse/lib/utilities";
import {
caretPosition,
inCodeBlock,
translateModKey,
} from "discourse/lib/utilities";
import discourseComputed, {
observes,
on,
@ -9,7 +13,7 @@ import { emojiUrlFor, generateCookFunction } from "discourse/lib/text";
import { later, schedule, scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import I18n from "I18n";
import Mousetrap from "mousetrap";
import ItsATrap from "@discourse/itsatrap";
import { Promise } from "rsvp";
import { SKIP } from "discourse/lib/autocomplete";
import { categoryHashtagTriggerRule } from "discourse/lib/category-hashtags";
@ -60,7 +64,7 @@ let _createCallbacks = [];
class Toolbar {
constructor(opts) {
const { siteSettings } = opts;
const { site, siteSettings } = opts;
this.shortcuts = {};
this.context = null;
@ -125,29 +129,31 @@ class Toolbar {
action: (...args) => this.context.send("formatCode", args),
});
this.addButton({
id: "bullet",
group: "extras",
icon: "list-ul",
shortcut: "Shift+8",
title: "composer.ulist_title",
preventFocus: true,
perform: (e) => e.applyList("* ", "list_item"),
});
if (!site.mobileView) {
this.addButton({
id: "bullet",
group: "extras",
icon: "list-ul",
shortcut: "Shift+8",
title: "composer.ulist_title",
preventFocus: true,
perform: (e) => e.applyList("* ", "list_item"),
});
this.addButton({
id: "list",
group: "extras",
icon: "list-ol",
shortcut: "Shift+7",
title: "composer.olist_title",
preventFocus: true,
perform: (e) =>
e.applyList(
(i) => (!i ? "1. " : `${parseInt(i, 10) + 1}. `),
"list_item"
),
});
this.addButton({
id: "list",
group: "extras",
icon: "list-ol",
shortcut: "Shift+7",
title: "composer.olist_title",
preventFocus: true,
perform: (e) =>
e.applyList(
(i) => (!i ? "1. " : `${parseInt(i, 10) + 1}. `),
"list_item"
),
});
}
if (siteSettings.support_mixed_text_direction) {
this.addButton({
@ -191,24 +197,12 @@ class Toolbar {
if (button.shortcut) {
const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
const mod = mac ? "Meta" : "Ctrl";
let shortcutTitle = `${mod}+${button.shortcut}`;
// Mac users are used to glyphs for shortcut keys
if (mac) {
shortcutTitle = shortcutTitle
.replace("Shift", "\u21E7")
.replace("Meta", "\u2318")
.replace("Alt", "\u2325")
.replace(/\+/g, "");
} else {
shortcutTitle = shortcutTitle
.replace("Shift", I18n.t("shortcut_modifier_key.shift"))
.replace("Ctrl", I18n.t("shortcut_modifier_key.ctrl"))
.replace("Alt", I18n.t("shortcut_modifier_key.alt"));
}
const shortcutTitle = `${translateModKey(mod + "+")}${translateModKey(
button.shortcut
)}`;
createdButton.title = `${title} (${shortcutTitle})`;
this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton;
} else {
createdButton.title = title;
@ -238,7 +232,7 @@ export default Component.extend(TextareaTextManipulation, {
classNames: ["d-editor"],
ready: false,
lastSel: null,
_mouseTrap: null,
_itsatrap: null,
showLink: true,
emojiPickerIsActive: false,
emojiStore: service("emoji-store"),
@ -271,6 +265,8 @@ export default Component.extend(TextareaTextManipulation, {
didInsertElement() {
this._super(...arguments);
this._previewMutationObserver = this._disablePreviewTabIndex();
this._textarea = this.element.querySelector("textarea.d-editor-input");
this._$textarea = $(this._textarea);
this._applyEmojiAutocomplete(this._$textarea);
@ -278,12 +274,12 @@ export default Component.extend(TextareaTextManipulation, {
scheduleOnce("afterRender", this, this._readyNow);
this._mouseTrap = new Mousetrap(this._textarea);
this._itsatrap = new ItsATrap(this._textarea);
const shortcuts = this.get("toolbar.shortcuts");
Object.keys(shortcuts).forEach((sc) => {
const button = shortcuts[sc];
this._mouseTrap.bind(sc, () => {
this._itsatrap.bind(sc, () => {
button.action(button);
return false;
});
@ -335,9 +331,13 @@ export default Component.extend(TextareaTextManipulation, {
this.appEvents.off("composer:replace-text", this, "_replaceText");
}
this._mouseTrap.reset();
this._itsatrap?.destroy();
this._itsatrap = null;
$(this.element.querySelector(".d-editor-preview")).off("click.preview");
this._previewMutationObserver?.disconnect();
if (isTesting()) {
this.element.removeEventListener("paste", this.paste);
}
@ -388,6 +388,8 @@ export default Component.extend(TextareaTextManipulation, {
this.set("preview", cooked);
let previewPromise = Promise.resolve();
if (this.siteSettings.enable_diffhtml_preview) {
const cookedElement = document.createElement("div");
cookedElement.innerHTML = cooked;
@ -405,40 +407,29 @@ export default Component.extend(TextareaTextManipulation, {
true
);
loadScript("/javascripts/diffhtml.min.js").then(() => {
// changing the contents of the preview element between two uses of
// diff.innerHTML did not apply the diff correctly
window.diff.release(this.element.querySelector(".d-editor-preview"));
previewPromise = loadScript("/javascripts/diffhtml.min.js").then(() => {
window.diff.innerHTML(
this.element.querySelector(".d-editor-preview"),
cookedElement.innerHTML,
{
parser: {
rawElements: ["script", "noscript", "style", "template"],
},
}
cookedElement.innerHTML
);
});
}
schedule("afterRender", () => {
if (this._state !== "inDOM" || !this.element) {
return;
}
previewPromise.then(() => {
schedule("afterRender", () => {
if (this._state !== "inDOM" || !this.element) {
return;
}
const preview = this.element.querySelector(".d-editor-preview");
if (!preview) {
return;
}
const preview = this.element.querySelector(".d-editor-preview");
if (!preview) {
return;
}
// prevents any tab focus in preview
preview.querySelectorAll("a").forEach((anchor) => {
anchor.setAttribute("tabindex", "-1");
if (this.previewUpdated) {
this.previewUpdated($(preview));
}
});
if (this.previewUpdated) {
this.previewUpdated($(preview));
}
});
});
},
@ -912,4 +903,21 @@ export default Component.extend(TextareaTextManipulation, {
this.set("isEditorFocused", false);
},
},
_disablePreviewTabIndex() {
const observer = new MutationObserver(function () {
document.querySelectorAll(".d-editor-preview a").forEach((anchor) => {
anchor.setAttribute("tabindex", "-1");
});
});
observer.observe(document.querySelector(".d-editor-preview"), {
childList: true,
subtree: true,
attributes: false,
characterData: true,
});
return observer;
},
});

View File

@ -8,7 +8,10 @@ export default Component.extend({
didInsertElement() {
this._super(...arguments);
$("#modal-alert").hide();
this._modalAlertElement = document.getElementById("modal-alert");
if (this._modalAlertElement) {
this._modalAlertElement.innerHTML = "";
}
let fixedParent = $(this.element).closest(".d-modal.fixed-modal");
if (fixedParent.length) {
@ -55,10 +58,10 @@ export default Component.extend({
},
_clearFlash() {
const modalAlert = document.getElementById("modal-alert");
if (modalAlert) {
modalAlert.style.display = "none";
modalAlert.classList.remove(
if (this._modalAlertElement) {
this._modalAlertElement.innerHTML = "";
this._modalAlertElement.classList.remove(
"alert",
"alert-error",
"alert-info",
"alert-success",
@ -69,10 +72,14 @@ export default Component.extend({
_flash(msg) {
this._clearFlash();
if (!this._modalAlertElement) {
return;
}
$("#modal-alert")
.addClass(`alert alert-${msg.messageClass || "success"}`)
.html(msg.text || "")
.fadeIn();
this._modalAlertElement.classList.add(
"alert",
`alert-${msg.messageClass || "success"}`
);
this._modalAlertElement.innerHTML = msg.text || "";
},
});

View File

@ -1,8 +1,7 @@
import { computed } from "@ember/object";
import Component from "@ember/component";
import I18n from "I18n";
import { next, schedule } from "@ember/runloop";
import { bind, on } from "discourse-common/utils/decorators";
import discourseComputed, { bind, on } from "discourse-common/utils/decorators";
export default Component.extend({
classNameBindings: [
@ -21,6 +20,7 @@ export default Component.extend({
submitOnEnter: true,
dismissable: true,
title: null,
titleAriaElementId: null,
subtitle: null,
role: "dialog",
headerClass: null,
@ -41,9 +41,17 @@ export default Component.extend({
// Inform screenreaders of the modal
"aria-modal": "true",
ariaLabelledby: computed("title", function () {
return this.title ? "discourse-modal-title" : null;
}),
@discourseComputed("title", "titleAriaElementId")
ariaLabelledby(title, titleAriaElementId) {
if (titleAriaElementId) {
return titleAriaElementId;
}
if (title) {
return "discourse-modal-title";
}
return;
},
@on("didInsertElement")
setUp() {

View File

@ -21,7 +21,7 @@ import {
thisWeekend,
} from "discourse/lib/time-utils";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import Mousetrap from "mousetrap";
import ItsATrap from "@discourse/itsatrap";
export default Component.extend({
statusType: readOnly("topicTimer.status_type"),
@ -43,12 +43,13 @@ export default Component.extend({
"autoCloseAfterLastPost"
),
duration: null,
_itsatrap: null,
init() {
this._super(...arguments);
KeyboardShortcuts.pause();
this._mousetrap = new Mousetrap();
this.set("_itsatrap", new ItsATrap());
this.set("duration", this.initialDuration);
},
@ -65,7 +66,9 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
this._mousetrap.reset();
this._itsatrap.destroy();
this.set("_itsatrap", null);
KeyboardShortcuts.unpause();
},

View File

@ -5,6 +5,7 @@ import I18n from "I18n";
import LogsNotice from "discourse/services/logs-notice";
import { bind } from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
import { htmlSafe } from "@ember/template";
const _pluginNotices = [];
@ -111,7 +112,9 @@ export default Component.extend({
const requiredText = I18n.t("wizard_required", {
url: getURL("/wizard"),
});
notices.push(Notice.create({ text: requiredText, id: "alert-wizard" }));
notices.push(
Notice.create({ text: htmlSafe(requiredText), id: "alert-wizard" })
);
}
if (
@ -214,7 +217,7 @@ export default Component.extend({
@bind
_handleLogsNoticeUpdate() {
const logNotice = Notice.create({
text: LogsNotice.currentProp("message"),
text: htmlSafe(LogsNotice.currentProp("message")),
id: "alert-logs-notice",
options: {
dismissable: true,

View File

@ -1,4 +1,5 @@
import Component from "@ember/component";
import deprecated from "discourse-common/lib/deprecated";
import UploadMixin from "discourse/mixins/upload";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
@ -14,6 +15,10 @@ export default Component.extend(UploadMixin, {
init() {
this._super(...arguments);
// TODO (martin) (2022-01-22) Remove this component.
deprecated(
"image-uploader will be removed in a future version, use uppy-image-uploader instead (the API is the same)"
);
this._applyLightbox();
},

View File

@ -83,6 +83,7 @@ export default Component.extend({
afterPatch() {},
eventDispatched(eventName, key, refreshArg) {
key = typeof key === "function" ? key(refreshArg) : key;
const onRefresh = camelize(eventName.replace(/:/, "-"));
this.dirtyKeys.keyDirty(key, { onRefresh, refreshArg });
this.queueRerender();

View File

@ -1,10 +1,18 @@
import afterTransition from "discourse/lib/after-transition";
import { propertyEqual } from "discourse/lib/computed";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import {
postUrl,
selectedElement,
selectedText,
setCaretPosition,
translateModKey,
} from "discourse/lib/utilities";
import Component from "@ember/component";
import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment";
import KeyEnterEscape from "discourse/mixins/key-enter-escape";
import Sharing from "discourse/lib/sharing";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
@ -28,11 +36,32 @@ function getQuoteTitle(element) {
return titleEl.textContent.trim().replace(/:$/, "");
}
export default Component.extend({
function fixQuotes(str) {
// u+201c “
// u+201d ”
return str.replace(/[\u201C\u201D]/g, '"');
}
function regexSafeStr(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export default Component.extend(KeyEnterEscape, {
classNames: ["quote-button"],
classNameBindings: ["visible"],
classNameBindings: ["visible", "_displayFastEditInput:fast-editing"],
visible: false,
privateCategory: alias("topic.category.read_restricted"),
editPost: null,
_isFastEditable: false,
_displayFastEditInput: false,
_fastEditInitalSelection: null,
_fastEditNewSelection: null,
_isSavingFastEdit: false,
_canEditPost: false,
_saveEditButtonTitle: I18n.t("composer.title", {
modifier: translateModKey("Meta+"),
}),
_isMouseDown: false,
_reselected: false,
@ -40,9 +69,18 @@ export default Component.extend({
_hideButton() {
this.quoteState.clear();
this.set("visible", false);
this.set("_isFastEditable", false);
this.set("_displayFastEditInput", false);
this.set("_fastEditInitalSelection", null);
this.set("_fastEditNewSelection", null);
},
_selectionChanged() {
if (this._displayFastEditInput) {
return;
}
const quoteState = this.quoteState;
const selection = window.getSelection();
@ -104,6 +142,31 @@ export default Component.extend({
quoteState.selected(postId, _selectedText, opts);
this.set("visible", quoteState.buffer.length > 0);
if (this.siteSettings.enable_fast_edit) {
this.set(
"_canEditPost",
this.topic.postStream.findLoadedPost(postId)?.can_edit
);
const regexp = new RegExp(regexSafeStr(quoteState.buffer), "gi");
const matches = postBody.match(regexp);
if (
quoteState.buffer.length < 1 ||
quoteState.buffer.includes("|") || // tables are too complex
quoteState.buffer.match(/\n/g) || // linebreaks are too complex
matches?.length > 1 // duplicates are too complex
) {
this.set("_isFastEditable", false);
this.set("_fastEditInitalSelection", null);
this.set("_fastEditNewSelection", null);
} else if (matches?.length === 1) {
this.set("_isFastEditable", true);
this.set("_fastEditInitalSelection", quoteState.buffer);
this.set("_fastEditNewSelection", quoteState.buffer);
}
}
// avoid hard loops in quote selection unconditionally
// this can happen if you triple click text in firefox
if (this._prevSelection === _selectedText) {
@ -160,15 +223,11 @@ export default Component.extend({
let top = markerOffset.top;
let left = markerOffset.left + Math.max(0, parentScrollLeft);
if (showAtEnd) {
const nearRightEdgeOfScreen =
$(window).width() - $quoteButton.outerWidth() < left + 10;
top = nearRightEdgeOfScreen ? top + 50 : top + 20;
top = top + 25;
left = Math.min(
left + 10,
$(window).width() - $quoteButton.outerWidth() - 10
window.innerWidth - this.element.clientWidth - 10
);
} else {
top = top - $quoteButton.outerHeight() - 5;
@ -192,6 +251,12 @@ export default Component.extend({
this._prevSelection = null;
this._isMouseDown = true;
this._reselected = false;
// prevents fast-edit input event to trigger mousedown
if (e.target.classList.contains("fast-edit-input")) {
return;
}
if (
$(e.target).closest(".quote-button, .create, .share, .reply-new")
.length === 0
@ -199,7 +264,12 @@ export default Component.extend({
this._hideButton();
}
})
.on("mouseup.quote-button", () => {
.on("mouseup.quote-button", (e) => {
// prevents fast-edit input event to trigger mouseup
if (e.target.classList.contains("fast-edit-input")) {
return;
}
this._prevSelection = null;
this._isMouseDown = false;
onSelectionChanged();
@ -209,6 +279,8 @@ export default Component.extend({
onSelectionChanged();
}
});
this.appEvents.on("quote-button:quote", this, "insertQuote");
this.appEvents.on("quote-button:edit", this, "_toggleFastEditForm");
},
willDestroyElement() {
@ -216,6 +288,8 @@ export default Component.extend({
.off("mousedown.quote-button")
.off("mouseup.quote-button")
.off("selectionchange.quote-button");
this.appEvents.off("quote-button:quote", this, "insertQuote");
this.appEvents.off("quote-button:edit", this, "_toggleFastEditForm");
},
@discourseComputed("topic.{isPrivateMessage,invisible,category}")
@ -264,11 +338,114 @@ export default Component.extend({
);
},
_saveFastEditDisabled: propertyEqual(
"_fastEditInitalSelection",
"_fastEditNewSelection"
),
@action
insertQuote() {
this.attrs.selectText().then(() => this._hideButton());
},
@action
_toggleFastEditForm() {
if (this._isFastEditable) {
this.toggleProperty("_displayFastEditInput");
schedule("afterRender", () => {
if (this.site.mobileView) {
this.element.style.left = `${
(window.innerWidth - this.element.clientWidth) / 2
}px`;
}
document.querySelector("#fast-edit-input")?.focus();
});
} else {
const postId = this.quoteState.postId;
const postModel = this.topic.postStream.findLoadedPost(postId);
return ajax(`/posts/${postModel.id}`, { type: "GET", cache: false }).then(
(result) => {
let bestIndex = 0;
const rows = result.raw.split("\n");
// selecting even a part of the text of a list item will include
// "* " at the beginning of the buffer, we remove it to be able
// to find it in row
const buffer = fixQuotes(
this.quoteState.buffer.split("\n")[0].replace(/^\* /, "")
);
rows.some((row, index) => {
if (row.length && row.includes(buffer)) {
bestIndex = index;
return true;
}
});
this?.editPost(postModel);
afterTransition(document.querySelector("#reply-control"), () => {
const textarea = document.querySelector(".d-editor-input");
if (!textarea || this.isDestroyed || this.isDestroying) {
return;
}
// best index brings us to one row before as slice start from 1
// we add 1 to be at the beginning of next line, unless we start from top
setCaretPosition(
textarea,
rows.slice(0, bestIndex).join("\n").length +
(bestIndex > 0 ? 1 : 0)
);
// ensures we correctly scroll to caret and reloads composer
// if we do another selection/edit
textarea.blur();
textarea.focus();
});
}
);
}
},
@action
_saveFastEdit() {
const postId = this.quoteState?.postId;
const postModel = this.topic.postStream.findLoadedPost(postId);
this.set("_isSavingFastEdit", true);
return ajax(`/posts/${postModel.id}`, { type: "GET", cache: false })
.then((result) => {
const newRaw = result.raw.replace(
fixQuotes(this._fastEditInitalSelection),
fixQuotes(this._fastEditNewSelection)
);
postModel
.save({ raw: newRaw })
.catch(popupAjaxError)
.finally(() => {
this.set("_isSavingFastEdit", false);
this._hideButton();
});
})
.catch(popupAjaxError);
},
@action
save() {
if (this._displayFastEditInput && !this._saveFastEditDisabled) {
this._saveFastEdit();
}
},
@action
cancelled() {
this._hideButton();
},
@action
share(source) {
Sharing.shareSource(source, {

View File

@ -80,7 +80,9 @@ export function addAdvancedSearchOptions(options) {
}
export default Component.extend({
classNames: ["search-advanced-options"],
tagName: "details",
attributeBindings: ["expandFilters:open"],
classNames: ["advanced-filters"],
category: null,
init() {
@ -116,6 +118,7 @@ export default Component.extend({
: inOptionsForAll(),
statusOptions: statusOptions(),
postTimeOptions: postTimeOptions(),
showAllTagsCheckbox: false,
});
},
@ -313,10 +316,10 @@ export default Component.extend({
const userInput = match[0].replace(REGEXP_TAGS_REPLACE, "");
if (existingInput !== userInput) {
this.set(
"searchedTerms.tags",
userInput.length !== 0 ? userInput.split(joinChar) : null
);
const updatedTags = userInput?.split(joinChar);
this.set("searchedTerms.tags", updatedTags);
this.set("showAllTagsCheckbox", !!(updatedTags.length > 1));
}
} else if (!tags) {
this.set("searchedTerms.tags", null);
@ -496,6 +499,9 @@ export default Component.extend({
searchTerm += ` tags:${tags}`;
}
if (tagFilter.length > 1) {
this.set("showAllTagsCheckbox", true);
}
this._updateSearchTerm(searchTerm);
} else if (match.length !== 0) {
searchTerm = searchTerm.replace(match[0], "");

View File

@ -1,5 +1,9 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
tagName: "div",
classNames: ["fps-result"],
classNameBindings: ["bulkSelectEnabled"],
attributeBindings: ["role"],
role: "listitem",
});

View File

@ -5,7 +5,7 @@ import PanEvents, {
import { cancel, later, schedule } from "@ember/runloop";
import Docking from "discourse/mixins/docking";
import MountWidget from "discourse/components/mount-widget";
import Mousetrap from "mousetrap";
import ItsATrap from "@discourse/itsatrap";
import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change";
import { observes } from "discourse-common/utils/decorators";
import { topicTitleDecorators } from "discourse/components/topic-title";
@ -24,7 +24,7 @@ const SiteHeaderComponent = MountWidget.extend(
_panMenuOffset: 0,
_scheduledRemoveAnimate: null,
_topic: null,
_mousetrap: null,
_itsatrap: null,
@observes(
"currentUser.unread_notifications",
@ -183,16 +183,21 @@ const SiteHeaderComponent = MountWidget.extend(
}
const offset = info.offset();
const headerRect = header.getBoundingClientRect(),
headerOffset = headerRect.top + headerRect.height,
doc = document.documentElement;
if (offset >= this.docAt) {
if (!this.dockedHeader) {
document.body.classList.add("docked");
this.dockedHeader = true;
doc.style.setProperty("--header-offset", `${headerOffset}px`);
}
} else {
if (this.dockedHeader) {
document.body.classList.remove("docked");
this.dockedHeader = false;
}
doc.style.setProperty("--header-offset", `${headerOffset}px`);
}
},
@ -258,8 +263,8 @@ const SiteHeaderComponent = MountWidget.extend(
}
const header = document.querySelector("header.d-header");
this._mousetrap = new Mousetrap(header);
this._mousetrap.bind(["right", "left"], (e) => {
this._itsatrap = new ItsATrap(header);
this._itsatrap.bind(["right", "left"], (e) => {
const activeTab = document.querySelector(".glyphs .menu-link.active");
if (activeTab) {
@ -294,7 +299,8 @@ const SiteHeaderComponent = MountWidget.extend(
cancel(this._scheduledRemoveAnimate);
this._mousetrap.reset();
this._itsatrap?.destroy();
this._itsatrap = null;
document.removeEventListener("click", this._dismissFirstNotification);
},

View File

@ -5,6 +5,7 @@ import Site from "discourse/models/site";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
tagName: "",
@ -18,13 +19,68 @@ export default Component.extend({
}
}),
@discourseComputed("topic", "topicTrackingState.messageCount")
@discourseComputed(
"topic",
"pmTopicTrackingState.isTracking",
"pmTopicTrackingState.statesModificationCounter",
"topicTrackingState.messageCount"
)
browseMoreMessage(topic) {
// TODO decide what to show for pms
if (topic.get("isPrivateMessage")) {
return;
}
return topic.isPrivateMessage
? this._privateMessageBrowseMoreMessage(topic)
: this._topicBrowseMoreMessage(topic);
},
_privateMessageBrowseMoreMessage(topic) {
const username = this.currentUser.username;
const suggestedGroupName = topic.suggested_group_name;
const inboxFilter = suggestedGroupName ? "group" : "user";
const unreadCount = this.pmTopicTrackingState.lookupCount("unread", {
inboxFilter: inboxFilter,
groupName: suggestedGroupName,
});
const newCount = this.pmTopicTrackingState.lookupCount("new", {
inboxFilter: inboxFilter,
groupName: suggestedGroupName,
});
if (unreadCount + newCount > 0) {
const hasBoth = unreadCount > 0 && newCount > 0;
if (suggestedGroupName) {
return I18n.messageFormat("user.messages.read_more_group_pm_MF", {
BOTH: hasBoth,
UNREAD: unreadCount,
NEW: newCount,
username: username,
groupName: suggestedGroupName,
groupLink: this._groupLink(username, suggestedGroupName),
basePath: getURL(""),
});
} else {
return I18n.messageFormat("user.messages.read_more_personal_pm_MF", {
BOTH: hasBoth,
UNREAD: unreadCount,
NEW: newCount,
username,
basePath: getURL(""),
});
}
} else if (suggestedGroupName) {
return I18n.t("user.messages.read_more_in_group", {
groupLink: this._groupLink(username, suggestedGroupName),
});
} else {
return I18n.t("user.messages.read_more", {
basePath: getURL(""),
username,
});
}
},
_topicBrowseMoreMessage(topic) {
const opts = {
latestLink: `<a href="${getURL("/latest")}">${I18n.t(
"topic.view_latest_topics"
@ -50,8 +106,13 @@ export default Component.extend({
"</a>";
}
const unreadTopics = this.topicTrackingState.countUnread();
const newTopics = this.currentUser ? this.topicTrackingState.countNew() : 0;
let unreadTopics = 0;
let newTopics = 0;
if (this.currentUser) {
unreadTopics = this.topicTrackingState.countUnread();
newTopics = this.topicTrackingState.countNew();
}
if (newTopics + unreadTopics > 0) {
const hasBoth = unreadTopics > 0 && newTopics > 0;
@ -71,4 +132,10 @@ export default Component.extend({
return I18n.t("topic.read_more", opts);
}
},
_groupLink(username, groupName) {
return `<a class="group-link" href="${getURL(
`/u/${username}/messages/group/${groupName}`
)}">${iconHTML("users")} ${groupName}</a>`;
},
});

View File

@ -67,6 +67,8 @@ export default Component.extend({
customDate: null,
customTime: null,
_itsatrap: null,
defaultCustomReminderTime: `0${START_OF_DAY_HOUR}:00`,
@on("init")
@ -77,6 +79,7 @@ export default Component.extend({
additionalOptionsToShow: this.additionalOptionsToShow || [],
hiddenOptions: this.hiddenOptions || [],
customOptions: this.customOptions || [],
customLabels: this.customLabels || {},
});
if (this.prefilledDatetime) {
@ -101,7 +104,8 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
this.mousetrap.unbind(Object.keys(BINDINGS));
this._itsatrap.unbind(Object.keys(BINDINGS));
},
parsePrefilledDatetime() {
@ -143,7 +147,7 @@ export default Component.extend({
_bindKeyboardShortcuts() {
Object.keys(BINDINGS).forEach((shortcut) => {
this.mousetrap.bind(shortcut, () => {
this._itsatrap.bind(shortcut, () => {
let binding = BINDINGS[shortcut];
this.send(binding.handler, ...binding.args);
return false;
@ -167,9 +171,16 @@ export default Component.extend({
"additionalOptionsToShow",
"hiddenOptions",
"customOptions",
"customLabels",
"userTimezone"
)
options(additionalOptionsToShow, hiddenOptions, customOptions, userTimezone) {
options(
additionalOptionsToShow,
hiddenOptions,
customOptions,
customLabels,
userTimezone
) {
this._loadLastUsedCustomDatetime();
let options = defaultShortcutOptions(userTimezone);
@ -223,6 +234,12 @@ export default Component.extend({
});
}
options.forEach((option) => {
if (customLabels[option.id]) {
option.label = customLabels[option.id];
}
});
return options;
},

View File

@ -1,16 +1,19 @@
import { alias, reads } from "@ember/object/computed";
import { alias, and, reads } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import LoadMore from "discourse/mixins/load-more";
import discourseDebounce from "discourse-common/lib/debounce";
import { on } from "@ember/object/evented";
import { schedule } from "@ember/runloop";
import showModal from "discourse/lib/show-modal";
export default Component.extend(LoadMore, {
tagName: "table",
classNames: ["topic-list"],
classNameBindings: ["bulkSelectEnabled:sticky-header"],
showTopicPostBadges: true,
listTitle: "topic.title",
canDoBulkActions: and("currentUser.staff", "selected.length"),
// Overwrite this to perform client side filtering of topics, if desired
filteredTopics: alias("topics"),
@ -162,6 +165,10 @@ export default Component.extend(LoadMore, {
);
},
updateAutoAddTopicsToBulkSelect(newVal) {
this.set("autoAddTopicsToBulkSelect", newVal);
},
click(e) {
let self = this;
let onClick = function (sel, callback) {
@ -191,6 +198,21 @@ export default Component.extend(LoadMore, {
this.changeSort(e2.data("sort-order"));
this.rerender();
});
onClick("button.bulk-select-actions", function () {
const controller = showModal("topic-bulk-actions", {
model: {
topics: this.selected,
category: this.category,
},
title: "topics.bulk.actions",
});
const action = this.bulkSelectAction;
if (action) {
controller.set("refreshClosure", () => action());
}
});
},
keyDown(e) {

View File

@ -105,7 +105,10 @@ export default MountWidget.extend(Docking, {
});
}
this.dispatch("topic:current-post-scrolled", "timeline-scrollarea");
this.dispatch(
"topic:current-post-scrolled",
() => `timeline-scrollarea-${this.topic.id}`
);
this.dispatch("topic:toggle-actions", "topic-admin-menu-button");
if (!this.site.mobileView) {
this.appEvents.on("composer:opened", this, this.queueRerender);

View File

@ -69,8 +69,6 @@ export default Component.extend(UppyUploadMixin, {
uploadDone(upload) {
this.setProperties({
imageUrl: upload.url,
imageId: upload.id,
imageFilesize: upload.human_filesize,
imageFilename: upload.original_filename,
imageWidth: upload.width,
@ -79,8 +77,13 @@ export default Component.extend(UppyUploadMixin, {
this._applyLightbox();
// the value of the property used for imageUrl should be set
// in this callback. this should be done in cases where imageUrl
// is bound to a computed property of the parent component.
if (this.onUploadDone) {
this.onUploadDone(upload);
} else {
this.set("imageUrl", upload.url);
}
},
@ -123,13 +126,16 @@ export default Component.extend(UppyUploadMixin, {
},
trash() {
this.setProperties({ imageUrl: null, imageId: null });
// uppy needs to be reset to allow for more uploads
this._reset();
// the value of the property used for imageUrl should be cleared
// in this callback. this should be done in cases where imageUrl
// is bound to a computed property of the parent component.
if (this.onUploadDeleted) {
this.onUploadDeleted();
} else {
this.setProperties({ imageUrl: null });
}
},
},

View File

@ -14,6 +14,7 @@ import { schedule } from "@ember/runloop";
export default Component.extend(LoadMore, {
tagName: "ul",
_lastDecoratedElement: null,
_initialize: on("init", function () {
const filter = this.get("stream.filter");
@ -47,6 +48,7 @@ export default Component.extend(LoadMore, {
$(this.element).on("click.discourse-redirect", ".excerpt a", (e) => {
return ClickTrack.trackClick(e, this.siteSettings);
});
this._updateLastDecoratedElement();
}),
// This view is being removed. Shut down operations
@ -59,6 +61,18 @@ export default Component.extend(LoadMore, {
$(this.element).off("click.discourse-redirect", ".excerpt a");
}),
_updateLastDecoratedElement() {
const nodes = this.element.querySelectorAll(".user-stream-item");
if (nodes.length === 0) {
return;
}
const lastElement = nodes[nodes.length - 1];
if (lastElement === this._lastDecoratedElement) {
return;
}
this._lastDecoratedElement = lastElement;
},
actions: {
removeBookmark(userAction) {
const stream = this.stream;
@ -123,7 +137,15 @@ export default Component.extend(LoadMore, {
this.set("loading", true);
const stream = this.stream;
stream.findItems().then(() => this.set("loading", false));
stream.findItems().then(() => {
this.set("loading", false);
let element = this._lastDecoratedElement?.nextElementSibling;
while (element) {
this.trigger("user-stream:new-item-inserted", element);
element = element.nextElementSibling;
}
this._updateLastDecoratedElement();
});
},
},
});

View File

@ -1,5 +1,4 @@
import Composer, { SAVE_ICONS, SAVE_LABELS } from "discourse/models/composer";
import { warn } from "@ember/debug";
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { action, computed } from "@ember/object";
import { alias, and, or, reads } from "@ember/object/computed";
@ -241,13 +240,22 @@ export default Controller.extend({
return SAVE_ICONS[modelAction];
},
// Note we update when some other attributes like tag/category change to allow
// text customizations to use those.
@discourseComputed(
"model.action",
"isWhispering",
"model.editConflict",
"model.privateMessage"
"model.privateMessage",
"model.tags",
"model.category"
)
saveLabel(modelAction, isWhispering, editConflict, privateMessage) {
let result = this.model.customizationFor("saveLabel");
if (result) {
return result;
}
if (editConflict) {
return "composer.overwrite_edit";
} else if (isWhispering) {
@ -285,22 +293,20 @@ export default Controller.extend({
return option;
},
@discourseComputed("model.isEncrypted")
composerComponent(isEncrypted) {
@discourseComputed()
composerComponent() {
const defaultComposer = "composer-editor";
if (this.siteSettings.enable_experimental_composer_uploader) {
if (isEncrypted) {
warn(
"Uppy cannot be used for composer uploads until upload handlers are developed, falling back to composer-editor.",
{ id: "composer" }
);
return defaultComposer;
}
return "composer-editor-uppy";
}
return defaultComposer;
},
@discourseComputed("model.requiredCategoryMissing", "model.replyLength")
disableTextarea(requiredCategoryMissing, replyLength) {
return requiredCategoryMissing && replyLength === 0;
},
@discourseComputed("model.composeState", "model.creatingTopic", "model.post")
popupMenuOptions(composeState) {
if (composeState === "open" || composeState === "fullscreen") {
@ -317,6 +323,28 @@ export default Controller.extend({
})
);
if (this.site.mobileView) {
options.push(
this._setupPopupMenuOption(() => {
return {
action: "applyUnorderedList",
icon: "list-ul",
label: "composer.ulist_title",
};
})
);
options.push(
this._setupPopupMenuOption(() => {
return {
action: "applyOrderedList",
icon: "list-ol",
label: "composer.olist_title",
};
})
);
}
options.push(
this._setupPopupMenuOption(() => {
return {
@ -678,6 +706,17 @@ export default Controller.extend({
});
});
},
applyUnorderedList() {
this.toolbarEvent.applyList("* ", "list_item");
},
applyOrderedList() {
this.toolbarEvent.applyList(
(i) => (!i ? "1. " : `${parseInt(i, 10) + 1}. `),
"list_item"
);
},
},
disableSubmit: or("model.loading", "isUploading", "isProcessingUpload"),

View File

@ -140,16 +140,19 @@ export default Controller.extend(
"serverAccountEmail",
"serverEmailValidation",
"accountEmail",
"rejectedEmails.[]"
"rejectedEmails.[]",
"forceValidationReason"
)
emailValidation(
serverAccountEmail,
serverEmailValidation,
email,
rejectedEmails
rejectedEmails,
forceValidationReason
) {
const failedAttrs = {
failed: true,
ok: false,
element: document.querySelector("#new-account-email"),
};
@ -162,6 +165,9 @@ export default Controller.extend(
return EmberObject.create(
Object.assign(failedAttrs, {
message: I18n.t("user.email.required"),
reason: forceValidationReason
? I18n.t("user.email.required")
: null,
})
);
}
@ -426,6 +432,7 @@ export default Controller.extend(
createAccount() {
this.clearFlash();
this.set("forceValidationReason", true);
const validation = [
this.emailValidation,
this.usernameValidation,
@ -435,23 +442,22 @@ export default Controller.extend(
].find((v) => v.failed);
if (validation) {
if (validation.message) {
this.flash(validation.message, "error");
}
const element = validation.element;
if (element.tagName === "DIV") {
if (element.scrollIntoView) {
element.scrollIntoView();
if (element) {
if (element.tagName === "DIV") {
if (element.scrollIntoView) {
element.scrollIntoView();
}
element.click();
} else {
element.focus();
}
element.click();
} else {
element.focus();
}
return;
}
this.set("forceValidationReason", false);
this.performAccountCreation();
},
},

View File

@ -9,7 +9,6 @@ export const queryParams = {
search: { replace: true, refreshModel: true },
max_posts: { replace: true, refreshModel: true },
q: { replace: true, refreshModel: true },
tags: { replace: true },
before: { replace: true, refreshModel: true },
bumped_before: { replace: true, refreshModel: true },
f: { replace: true, refreshModel: true },

View File

@ -42,5 +42,13 @@ export default DiscoveryController.extend({
refresh() {
this.send("triggerRefresh");
},
showInserted() {
const tracker = this.topicTrackingState;
// Move inserted into topics
this.model.loadBefore(tracker.get("newIncoming"), true);
tracker.resetTracking();
return false;
},
},
});

View File

@ -0,0 +1,26 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { downloadGoogle, downloadIcs } from "discourse/lib/download-calendar";
export default Controller.extend(ModalFunctionality, {
selectedCalendar: "ics",
remember: false,
@action
downloadCalendar() {
if (this.remember) {
this.currentUser.setProperties({
default_calendar: this.selectedCalendar,
user_option: { default_calendar: this.selectedCalendar },
});
this.currentUser.save(["default_calendar"]);
}
if (this.selectedCalendar === "ics") {
downloadIcs(this.model.title, this.model.dates);
} else {
downloadGoogle(this.model.title, this.model.dates);
}
this.send("closeModal");
},
});

View File

@ -54,14 +54,23 @@ export default Controller.extend(ModalFunctionality, {
const accountEmailOrUsername = escapeExpression(
this.accountEmailOrUsername
);
const isEmail = accountEmailOrUsername.match(/@/);
let key = `forgot_password.complete_${
isEmail ? "email" : "username"
}`;
let extraClass;
if (data.user_found === true) {
key += "_found";
let key = "forgot_password.complete";
key += accountEmailOrUsername.match(/@/) ? "_email" : "_username";
if (data.user_found === false) {
key += "_not_found";
this.flash(
I18n.t(key, {
email: accountEmailOrUsername,
username: accountEmailOrUsername,
}),
"error"
);
} else {
key += data.user_found ? "_found" : "";
this.set("accountEmailOrUsername", "");
this.set(
"offerHelp",
@ -70,19 +79,7 @@ export default Controller.extend(ModalFunctionality, {
username: accountEmailOrUsername,
})
);
} else {
if (data.user_found === false) {
key += "_not_found";
extraClass = "error";
}
this.flash(
I18n.t(key, {
email: accountEmailOrUsername,
username: accountEmailOrUsername,
}),
extraClass
);
this.set("helpSeen", !data.user_found);
}
})
.catch((e) => {

View File

@ -15,6 +15,10 @@ import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { scrollTop } from "discourse/mixins/scroll-top";
import { setTransient } from "discourse/lib/page-tracker";
import { Promise } from "rsvp";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import showModal from "discourse/lib/show-modal";
import userSearch from "discourse/lib/user-search";
const SortOrders = [
{ name: I18n.t("search.relevance"), id: 0 },
@ -23,6 +27,11 @@ const SortOrders = [
{ name: I18n.t("search.most_viewed"), id: 3, term: "order:views" },
{ name: I18n.t("search.latest_topic"), id: 4, term: "order:latest_topic" },
];
export const SEARCH_TYPE_DEFAULT = "topics_posts";
export const SEARCH_TYPE_CATS_TAGS = "categories_tags";
export const SEARCH_TYPE_USERS = "users";
const PAGE_LIMIT = 10;
export default Controller.extend({
@ -31,11 +40,17 @@ export default Controller.extend({
bulkSelectEnabled: null,
loading: false,
queryParams: ["q", "expanded", "context_id", "context", "skip_context"],
q: null,
selected: [],
expanded: false,
queryParams: [
"q",
"expanded",
"context_id",
"context",
"skip_context",
"search_type",
],
q: undefined,
context_id: null,
search_type: SEARCH_TYPE_DEFAULT,
context: null,
searching: false,
sortOrder: 0,
@ -43,12 +58,34 @@ export default Controller.extend({
invalidSearch: false,
page: 1,
resultCount: null,
searchTypes: null,
init() {
this._super(...arguments);
this.set("searchTypes", [
{ name: I18n.t("search.type.default"), id: SEARCH_TYPE_DEFAULT },
{
name: this.siteSettings.tagging_enabled
? I18n.t("search.type.categories_and_tags")
: I18n.t("search.type.categories"),
id: SEARCH_TYPE_CATS_TAGS,
},
{ name: I18n.t("search.type.users"), id: SEARCH_TYPE_USERS },
]);
this.selected = [];
},
@discourseComputed("resultCount")
hasResults(resultCount) {
return (resultCount || 0) > 0;
},
@discourseComputed("expanded")
expandFilters(expanded) {
return expanded === "true";
},
@discourseComputed("q")
hasAutofocus(q) {
return isEmpty(q);
@ -138,6 +175,14 @@ export default Controller.extend({
}
},
@observes("search_type")
triggerSearchOnTypeChange() {
if (this.searchActive) {
this.set("page", 1);
this._search();
}
},
@observes("model")
modelChanged() {
if (this.searchTerm !== this.q) {
@ -182,9 +227,19 @@ export default Controller.extend({
return I18n.t("search.result_count", { count, plus, term });
},
@observes("model.posts.length")
@observes("model.[posts,categories,tags,users].length")
resultCountChanged() {
this.set("resultCount", this.get("model.posts.length"));
if (!this.model.posts) {
return 0;
}
this.set(
"resultCount",
this.model.posts.length +
this.model.categories.length +
this.model.tags.length +
this.model.users.length
);
},
@discourseComputed("hasResults")
@ -202,6 +257,18 @@ export default Controller.extend({
return page === PAGE_LIMIT;
},
@discourseComputed("search_type")
usingDefaultSearchType(searchType) {
return searchType === SEARCH_TYPE_DEFAULT;
},
@discourseComputed("bulkSelectEnabled")
searchInfoClassNames(bulkSelectEnabled) {
return bulkSelectEnabled
? "search-info bulk-select-visible"
: "search-info";
},
searchButtonDisabled: or("searching", "loading"),
_search() {
@ -244,33 +311,71 @@ export default Controller.extend({
const searchKey = getSearchKey(args);
ajax("/search", { data: args })
.then(async (results) => {
const model = (await translateResults(results)) || {};
switch (this.search_type) {
case SEARCH_TYPE_CATS_TAGS:
const categoryTagSearch = searchCategoryTag(
searchTerm,
this.siteSettings
);
Promise.resolve(categoryTagSearch)
.then(async (results) => {
const categories = results.filter((c) => Boolean(c.model));
const tags = results.filter((c) => !Boolean(c.model));
const model = (await translateResults({ categories, tags })) || {};
this.set("model", model);
})
.finally(() => {
this.setProperties({
searching: false,
loading: false,
});
});
break;
case SEARCH_TYPE_USERS:
userSearch({ term: searchTerm, limit: 20 })
.then(async (results) => {
const model = (await translateResults({ users: results })) || {};
this.set("model", model);
})
.finally(() => {
this.setProperties({
searching: false,
loading: false,
});
});
break;
default:
ajax("/search", { data: args })
.then(async (results) => {
const model = (await translateResults(results)) || {};
if (results.grouped_search_result) {
this.set("q", results.grouped_search_result.term);
}
if (results.grouped_search_result) {
this.set("q", results.grouped_search_result.term);
}
if (args.page > 1) {
if (model) {
this.model.posts.pushObjects(model.posts);
this.model.topics.pushObjects(model.topics);
this.model.set(
"grouped_search_result",
results.grouped_search_result
);
}
} else {
setTransient("lastSearch", { searchKey, model }, 5);
model.grouped_search_result = results.grouped_search_result;
this.set("model", model);
}
})
.finally(() => {
this.set("searching", false);
this.set("loading", false);
});
if (args.page > 1) {
if (model) {
this.model.posts.pushObjects(model.posts);
this.model.topics.pushObjects(model.topics);
this.model.set(
"grouped_search_result",
results.grouped_search_result
);
}
} else {
setTransient("lastSearch", { searchKey, model }, 5);
model.grouped_search_result = results.grouped_search_result;
this.set("model", model);
}
})
.finally(() => {
this.setProperties({
searching: false,
loading: false,
});
});
break;
}
},
actions: {
@ -309,16 +414,25 @@ export default Controller.extend({
this.selected.clear();
},
search() {
this.set("page", 1);
this._search();
if (this.site.mobileView) {
this.set("expanded", false);
}
showBulkActions() {
const modalController = showModal("topic-bulk-actions", {
model: {
topics: this.selected,
},
title: "topics.bulk.actions",
});
modalController.set("refreshClosure", () => this._search());
},
toggleAdvancedSearch() {
this.toggleProperty("expanded");
search(options = {}) {
if (options.collapseFilters) {
document
.querySelector("details.advanced-filters")
?.removeAttribute("open");
}
this.set("page", 1);
this._search();
},
loadMore() {

View File

@ -0,0 +1,83 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { getWebauthnCredential } from "discourse/lib/webauthn";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
import I18n from "I18n";
export default Controller.extend(ModalFunctionality, {
showSecondFactor: false,
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
secondFactorToken: null,
securityKeyCredential: null,
inProgress: false,
onShow() {
this.setProperties({
showSecondFactor: false,
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
secondFactorToken: null,
securityKeyCredential: null,
});
},
@discourseComputed("inProgress", "securityKeyCredential", "secondFactorToken")
disabled(inProgress, securityKeyCredential, secondFactorToken) {
return inProgress || (!securityKeyCredential && !secondFactorToken);
},
setResult(result) {
this.setProperties({
otherMethodAllowed: result.multiple_second_factor_methods,
secondFactorRequired: true,
showLoginButtons: false,
backupEnabled: result.backup_enabled,
showSecondFactor: result.totp_enabled,
showSecurityKey: result.security_key_enabled,
secondFactorMethod: result.security_key_enabled
? SECOND_FACTOR_METHODS.SECURITY_KEY
: SECOND_FACTOR_METHODS.TOTP,
securityKeyChallenge: result.challenge,
securityKeyAllowedCredentialIds: result.allowed_credential_ids,
});
},
@action
authenticateSecurityKey() {
getWebauthnCredential(
this.securityKeyChallenge,
this.securityKeyAllowedCredentialIds,
(credentialData) => {
this.set("securityKeyCredential", credentialData);
this.send("authenticate");
},
(errorMessage) => {
this.flash(errorMessage, "error");
}
);
},
@action
authenticate() {
this.set("inProgress", true);
this.model
.grantAdmin({
second_factor_token:
this.securityKeyCredential || this.secondFactorToken,
second_factor_method: this.secondFactorMethod,
timezone: moment.tz.guess(),
})
.then((result) => {
if (result.success) {
this.send("closeModal");
bootbox.alert(I18n.t("admin.user.grant_admin_success"));
} else {
this.flash(result.error, "error");
this.setResult(result);
}
})
.finally(() => this.set("inProgress", false));
},
});

View File

@ -87,12 +87,16 @@ export default Controller.extend({
return defaultTabs;
},
@discourseComputed("model.is_group_user")
showMessages(isGroupUser) {
@discourseComputed("model.has_messages", "model.is_group_user")
showMessages(hasMessages, isGroupUser) {
if (!this.siteSettings.enable_personal_messages) {
return false;
}
if (!hasMessages) {
return false;
}
return isGroupUser || (this.currentUser && this.currentUser.admin);
},

View File

@ -1,11 +1,12 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import { translateModKey } from "discourse/lib/utilities";
import ModalFunctionality from "discourse/mixins/modal-functionality";
const KEY = "keyboard_shortcuts_help";
const SHIFT = I18n.t("shortcut_modifier_key.shift");
const ALT = I18n.t("shortcut_modifier_key.alt");
const ALT = translateModKey("Alt");
const CTRL = I18n.t("shortcut_modifier_key.ctrl");
const ENTER = I18n.t("shortcut_modifier_key.enter");
@ -210,7 +211,7 @@ export default Controller.extend(ModalFunctionality, {
keys1: ["m", "w"],
}),
print: buildShortcut("actions.print", {
keys1: [CTRL, "p"],
keys1: [translateModKey("Meta"), "p"],
keysDelimiter: PLUS,
}),
defer: buildShortcut("actions.defer", {

View File

@ -428,7 +428,10 @@ export default Controller.extend(ModalFunctionality, {
});
next(() => {
showModal("createAccount", { modalClass: "create-account" });
showModal("createAccount", {
modalClass: "create-account",
titleAriaElementId: "create-account-title",
});
});
},
});

View File

@ -26,6 +26,7 @@ const USER_HOMES = {
4: "new",
5: "top",
6: "bookmarks",
7: "unseen",
};
const TEXT_SIZES = ["smallest", "smaller", "normal", "larger", "largest"];
@ -242,12 +243,12 @@ export default Controller.extend({
return;
}
const defaultTheme = this.site.user_themes?.findBy("default", true);
const theme = this.userSelectableThemes?.findBy("id", this.themeId);
// we don't want to display the numeric ID of a scheme
// when it is set by the theme but not marked as user selectable
if (
defaultTheme?.color_scheme_id === this.session.userColorSchemeId &&
theme?.color_scheme_id === this.session.userColorSchemeId &&
!this.userSelectableColorSchemes.findBy(
"id",
this.session.userColorSchemeId

View File

@ -23,6 +23,12 @@ export default Controller.extend({
"card_background_upload_url",
"date_of_birth",
"timezone",
"default_calendar",
];
this.calendarOptions = [
{ name: I18n.t("download_calendar.google"), value: "google" },
{ name: I18n.t("download_calendar.ics"), value: "ics" },
];
},
@ -45,6 +51,11 @@ export default Controller.extend({
}
},
@discourseComputed("model.default_calendar")
canChangeDefaultCalendar(defaultCalendar) {
return defaultCalendar !== "none_selected";
},
canChangeBio: readOnly("model.can_change_bio"),
canChangeLocation: readOnly("model.can_change_location"),
@ -57,10 +68,6 @@ export default Controller.extend({
"model.can_upload_user_card_background"
),
experimentalUserCardImageUpload: readOnly(
"siteSettings.enable_experimental_image_uploader"
),
actions: {
showFeaturedTopicModal() {
showModal("feature-topic-on-profile", {

View File

@ -99,7 +99,7 @@ addBulkButton("removeTags", "remove_tags", {
});
addBulkButton("deleteTopics", "delete", {
icon: "trash-alt",
class: "btn-danger",
class: "btn-danger delete-topics",
buttonVisible: function () {
return this.currentUser.staff;
},

View File

@ -158,8 +158,6 @@ export default Controller.extend(bufferedProperty("model"), {
if (name) {
url = `${url}/group/${name}`;
} else {
url = `${url}/personal`;
}
DiscourseURL.routeTo(url);
@ -211,19 +209,35 @@ export default Controller.extend(bufferedProperty("model"), {
},
_removeDeleteOnOwnerReplyBookmarks() {
// the user has already navigated away from the topic. the PostCreator
// in rails already handles deleting the bookmarks that need to be
// based on auto_delete_preference; this is mainly used to clean up
// the in-memory post stream and topic model
if (!this.model) {
return;
}
const posts = this.get("model.postStream.posts");
if (posts) {
posts
.filter(
(p) =>
p.bookmarked &&
p.bookmark_auto_delete_preference ===
(post) =>
post.bookmarked &&
post.bookmark_auto_delete_preference ===
AUTO_DELETE_PREFERENCES.ON_OWNER_REPLY
)
.forEach((p) => {
p.clearBookmark();
.forEach((post) => {
post.clearBookmark();
this.model.removeBookmark(post.bookmark_id);
});
}
const forTopicBookmark = this.model.bookmarks.findBy("for_topic", true);
if (
forTopicBookmark?.auto_delete_preference ===
AUTO_DELETE_PREFERENCES.ON_OWNER_REPLY
) {
this.model.removeBookmark(forTopicBookmark.id);
}
},
_forceRefreshPostStream() {
@ -581,9 +595,9 @@ export default Controller.extend(bufferedProperty("model"), {
post.get("post_number") === 1 ? this.recoverTopic() : post.recover();
},
deletePost(post) {
deletePost(post, opts) {
if (post.get("post_number") === 1) {
return this.deleteTopic();
return this.deleteTopic(opts);
} else if (!post.can_delete) {
return false;
}
@ -597,7 +611,7 @@ export default Controller.extend(bufferedProperty("model"), {
ajax(`/posts/${post.id}/reply-ids.json`).then((replies) => {
if (replies.length === 0) {
return post
.destroy(user)
.destroy(user, opts)
.then(refresh)
.catch((error) => {
popupAjaxError(error);
@ -616,7 +630,7 @@ export default Controller.extend(bufferedProperty("model"), {
label: I18n.t("post.controls.delete_replies.just_the_post"),
callback() {
post
.destroy(user)
.destroy(user, opts)
.then(refresh)
.catch((error) => {
popupAjaxError(error);
@ -671,7 +685,7 @@ export default Controller.extend(bufferedProperty("model"), {
});
} else {
return post
.destroy(user)
.destroy(user, opts)
.then(refresh)
.catch((error) => {
popupAjaxError(error);
@ -680,6 +694,19 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
permanentlyDeletePost(post) {
return bootbox.confirm(
I18n.t("post.controls.permanently_delete_confirmation"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.send("deletePost", post, { force_destroy: true });
}
}
);
},
editPost(post) {
if (!this.currentUser) {
return bootbox.alert(I18n.t("post.controls.edit_anonymous"));
@ -723,9 +750,15 @@ export default Controller.extend(bufferedProperty("model"), {
if (!this.currentUser) {
return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
} else if (post) {
return this._togglePostBookmark(post);
const bookmarkForPost = this.model.bookmarks.find(
(bookmark) => bookmark.post_id === post.id && !bookmark.for_topic
);
return this._modifyPostBookmark(
bookmarkForPost || { post_id: post.id, for_topic: false },
post
);
} else {
return this._toggleTopicBookmark(this.model).then((changedIds) => {
return this._toggleTopicLevelBookmark().then((changedIds) => {
if (!changedIds) {
return;
}
@ -1189,110 +1222,152 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
_togglePostBookmark(post) {
_modifyTopicBookmark(bookmark) {
const title = bookmark.id
? "post.bookmarks.edit_for_topic"
: "post.bookmarks.create_for_topic";
return this._openBookmarkModal(bookmark, title, {
onAfterSave: () => {
this.model.set("bookmarked", true);
this.model.incrementProperty("bookmarksWereChanged");
this.appEvents.trigger("topic:bookmark-toggled");
},
});
},
_modifyPostBookmark(bookmark, post) {
const title = bookmark.id ? "post.bookmarks.edit" : "post.bookmarks.create";
return this._openBookmarkModal(bookmark, title, {
onCloseWithoutSaving: () => {
post.appEvents.trigger("post-stream:refresh", {
id: bookmark.post_id,
});
},
onAfterSave: (savedData) => {
post.createBookmark(savedData);
this.model.afterPostBookmarked(post, savedData);
return [post.id];
},
onAfterDelete: (topicBookmarked) => {
post.deleteBookmark(topicBookmarked);
},
});
},
_openBookmarkModal(
bookmark,
title,
callbacks = {
onCloseWithoutSaving: null,
onAfterSave: null,
onAfterDelete: null,
}
) {
return new Promise((resolve) => {
let modalController = showModal("bookmark", {
model: {
postId: post.id,
id: post.bookmark_id,
reminderAt: post.bookmark_reminder_at,
autoDeletePreference: post.bookmark_auto_delete_preference,
name: post.bookmark_name,
postId: bookmark.post_id,
id: bookmark.id,
reminderAt: bookmark.reminder_at,
autoDeletePreference: bookmark.auto_delete_preference,
name: bookmark.name,
forTopic: bookmark.for_topic,
},
title: post.bookmark_id
? "post.bookmarks.edit"
: "post.bookmarks.create",
title,
modalClass: "bookmark-with-reminder",
});
modalController.setProperties({
onCloseWithoutSaving: () => {
resolve({ closedWithoutSaving: true });
post.appEvents.trigger("post-stream:refresh", { id: post.id });
if (callbacks.onCloseWithoutSaving) {
callbacks.onCloseWithoutSaving();
}
resolve();
},
afterSave: (savedData) => {
this._addOrUpdateBookmarkedPost(post.id, savedData.reminderAt);
post.createBookmark(savedData);
resolve({ closedWithoutSaving: false });
this._syncBookmarks(savedData);
this.model.set("bookmarking", false);
let resolveData;
if (callbacks.onAfterSave) {
resolveData = callbacks.onAfterSave(savedData);
}
resolve(resolveData);
},
afterDelete: (topicBookmarked) => {
this.model.set(
"bookmarked_posts",
this.model.bookmarked_posts.filter((x) => x.post_id !== post.id)
);
post.deleteBookmark(topicBookmarked);
afterDelete: (topicBookmarked, bookmarkId) => {
this.model.removeBookmark(bookmarkId);
if (callbacks.onAfterDelete) {
callbacks.onAfterDelete(topicBookmarked);
}
resolve();
},
});
});
},
_addOrUpdateBookmarkedPost(postId, reminderAt) {
if (!this.model.bookmarked_posts) {
this.model.set("bookmarked_posts", []);
_syncBookmarks(data) {
if (!this.model.bookmarks) {
this.model.set("bookmarks", []);
}
let bookmarkedPost = this.model.bookmarked_posts.findBy("post_id", postId);
if (!bookmarkedPost) {
bookmarkedPost = { post_id: postId };
this.model.bookmarked_posts.pushObject(bookmarkedPost);
const bookmark = this.model.bookmarks.findBy("id", data.id);
if (!bookmark) {
this.model.bookmarks.pushObject(data);
} else {
bookmark.reminder_at = data.reminder_at;
bookmark.name = data.name;
bookmark.auto_delete_preference = data.auto_delete_preference;
}
bookmarkedPost.reminder_at = reminderAt;
},
_toggleTopicBookmark() {
async _toggleTopicLevelBookmark() {
if (this.model.bookmarking) {
return Promise.resolve();
}
this.model.set("bookmarking", true);
const bookmarkedPostsCount = this.model.bookmarked_posts
? this.model.bookmarked_posts.length
: 0;
const bookmarkPost = async (post) => {
const opts = await this._togglePostBookmark(post);
this.model.set("bookmarking", false);
if (opts.closedWithoutSaving) {
return;
}
this.model.afterPostBookmarked(post);
return [post.id];
};
if (this.model.bookmarkCount > 1) {
return this._maybeClearAllBookmarks();
}
const toggleBookmarkOnServer = async () => {
if (bookmarkedPostsCount === 0) {
const firstPost = await this.model.firstPost();
return bookmarkPost(firstPost);
} else if (bookmarkedPostsCount === 1) {
const postId = this.model.bookmarked_posts[0].post_id;
const post = await this.model.postById(postId);
return bookmarkPost(post);
if (this.model.bookmarkCount === 1) {
const forTopicBookmark = this.model.bookmarks.findBy("for_topic", true);
if (forTopicBookmark) {
return this._modifyTopicBookmark(forTopicBookmark);
} else {
return this.model
.deleteBookmarks()
.then(() => this.model.clearBookmarks())
.catch(popupAjaxError)
.finally(() => this.model.set("bookmarking", false));
const bookmark = this.model.bookmarks[0];
const post = await this.model.postById(bookmark.post_id);
return this._modifyPostBookmark(bookmark, post);
}
};
}
if (this.model.bookmarkCount === 0) {
const firstPost = await this.model.firstPost();
return this._modifyTopicBookmark({
post_id: firstPost.id,
for_topic: true,
});
}
},
_maybeClearAllBookmarks() {
return new Promise((resolve) => {
if (bookmarkedPostsCount > 1) {
bootbox.confirm(
I18n.t("bookmarks.confirm_clear"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
toggleBookmarkOnServer().then(resolve);
} else {
this.model.set("bookmarking", false);
resolve();
}
bootbox.confirm(
I18n.t("bookmarks.confirm_clear"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
return this.model
.deleteBookmarks()
.then(() => resolve(this.model.clearBookmarks()))
.catch(popupAjaxError)
.finally(() => {
this.model.set("bookmarking", false);
});
} else {
this.model.set("bookmarking", false);
resolve();
}
);
} else {
toggleBookmarkOnServer().then(resolve);
}
}
);
});
},
@ -1435,13 +1510,13 @@ export default Controller.extend(bufferedProperty("model"), {
this.model.recover();
},
deleteTopic() {
deleteTopic(opts) {
if (
this.model.views > this.siteSettings.min_topic_views_for_delete_confirm
) {
this.deleteTopicModal();
} else {
this.model.destroy(this.currentUser);
this.model.destroy(this.currentUser, opts);
}
},

View File

@ -3,42 +3,31 @@ import { iconHTML } from "discourse-common/lib/icon-library";
import Bookmark from "discourse/models/bookmark";
import I18n from "I18n";
import { Promise } from "rsvp";
import EmberObject, { action } from "@ember/object";
import EmberObject, { action, computed } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { notEmpty } from "@ember/object/computed";
import { equal, notEmpty } from "@ember/object/computed";
import { ajax } from "discourse/lib/ajax";
export default Controller.extend({
queryParams: ["q"],
q: null,
application: controller(),
user: controller(),
content: null,
loading: false,
loadingMore: false,
permissionDenied: false,
searchTerm: null,
q: null,
inSearchMode: notEmpty("q"),
noContent: equal("model.bookmarks.length", 0),
loadItems() {
this.setProperties({
content: [],
loading: true,
permissionDenied: false,
searchTerm: this.q,
});
return this.model
.loadItems({ q: this.q })
.then((response) => this._processLoadResponse(response))
.catch(() => this._bookmarksListDenied())
.finally(() => {
this.setProperties({
loaded: true,
loading: false,
});
});
},
searchTerm: computed("q", {
get() {
return this.q;
},
set(key, value) {
return value;
},
}),
@discourseComputed()
emptyStateBody() {
@ -57,20 +46,16 @@ export default Controller.extend({
return inSearchMode && noContent;
},
@discourseComputed("loaded", "content.length")
noContent(loaded, contentLength) {
return loaded && contentLength === 0;
},
@action
search() {
this.set("q", this.searchTerm);
this.loadItems();
this.transitionToRoute({
queryParams: { q: this.searchTerm },
});
},
@action
reload() {
this.loadItems();
this.send("triggerRefresh");
},
@action
@ -81,13 +66,27 @@ export default Controller.extend({
this.set("loadingMore", true);
return this.model
.loadMore({ q: this.q })
return this._loadMoreBookmarks(this.q)
.then((response) => this._processLoadResponse(response))
.catch(() => this._bookmarksListDenied())
.finally(() => this.set("loadingMore", false));
},
_loadMoreBookmarks(searchQuery) {
if (!this.model.loadMoreUrl) {
return Promise.resolve();
}
let moreUrl = this.model.loadMoreUrl;
if (searchQuery) {
const delimiter = moreUrl.includes("?") ? "&" : "?";
const q = encodeURIComponent(searchQuery);
moreUrl += `${delimiter}q=${q}`;
}
return ajax({ url: moreUrl });
},
_bookmarksListDenied() {
this.set("permissionDenied", true);
},
@ -98,22 +97,24 @@ export default Controller.extend({
}
response = response.user_bookmark_list;
this.model.more_bookmarks_url = response.more_bookmarks_url;
this.model.loadMoreUrl = response.more_bookmarks_url;
if (response.bookmarks) {
const bookmarkModels = response.bookmarks.map((bookmark) => {
const bookmarkModel = Bookmark.create(bookmark);
bookmarkModel.topicStatus = EmberObject.create({
closed: bookmark.closed,
archived: bookmark.archived,
is_warning: bookmark.is_warning,
pinned: false,
unpinned: false,
invisible: bookmark.invisible,
});
return bookmarkModel;
});
this.content.pushObjects(bookmarkModels);
const bookmarkModels = response.bookmarks.map(this.transform);
this.model.bookmarks.pushObjects(bookmarkModels);
}
},
transform(bookmark) {
const bookmarkModel = Bookmark.create(bookmark);
bookmarkModel.topicStatus = EmberObject.create({
closed: bookmark.closed,
archived: bookmark.archived,
is_warning: bookmark.is_warning,
pinned: false,
unpinned: false,
invisible: bookmark.invisible,
});
return bookmarkModel;
},
});

View File

@ -62,39 +62,6 @@ export default Controller.extend({
return invitesCountTotal > 0;
},
@discourseComputed("invitesCount.total", "invitesCount.pending")
pendingLabel(invitesCountTotal, invitesCountPending) {
if (invitesCountTotal > 0) {
return I18n.t("user.invited.pending_tab_with_count", {
count: invitesCountPending,
});
} else {
return I18n.t("user.invited.pending_tab");
}
},
@discourseComputed("invitesCount.total", "invitesCount.expired")
expiredLabel(invitesCountTotal, invitesCountExpired) {
if (invitesCountTotal > 0) {
return I18n.t("user.invited.expired_tab_with_count", {
count: invitesCountExpired,
});
} else {
return I18n.t("user.invited.expired_tab");
}
},
@discourseComputed("invitesCount.total", "invitesCount.redeemed")
redeemedLabel(invitesCountTotal, invitesCountRedeemed) {
if (invitesCountTotal > 0) {
return I18n.t("user.invited.redeemed_tab_with_count", {
count: invitesCountRedeemed,
});
} else {
return I18n.t("user.invited.redeemed_tab");
}
},
@action
createInvite() {
const controller = showModal("create-invite");

View File

@ -0,0 +1,38 @@
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Controller.extend({
@discourseComputed("invitesCount.total", "invitesCount.pending")
pendingLabel(invitesCountTotal, invitesCountPending) {
if (invitesCountTotal > 0) {
return I18n.t("user.invited.pending_tab_with_count", {
count: invitesCountPending,
});
} else {
return I18n.t("user.invited.pending_tab");
}
},
@discourseComputed("invitesCount.total", "invitesCount.expired")
expiredLabel(invitesCountTotal, invitesCountExpired) {
if (invitesCountTotal > 0) {
return I18n.t("user.invited.expired_tab_with_count", {
count: invitesCountExpired,
});
} else {
return I18n.t("user.invited.expired_tab");
}
},
@discourseComputed("invitesCount.total", "invitesCount.redeemed")
redeemedLabel(invitesCountTotal, invitesCountRedeemed) {
if (invitesCountTotal > 0) {
return I18n.t("user.invited.redeemed_tab_with_count", {
count: invitesCountRedeemed,
});
} else {
return I18n.t("user.invited.redeemed_tab");
}
},
});

View File

@ -6,14 +6,14 @@ import { VIEW_NAME_WARNINGS } from "discourse/routes/user-private-messages-warni
import I18n from "I18n";
export const PERSONAL_INBOX = "__personal_inbox__";
const ALL_INBOX = "__all_inbox__";
export default Controller.extend({
user: controller(),
pmView: false,
viewingSelf: alias("user.viewingSelf"),
isGroup: equal("pmView", "groups"),
isGroup: equal("pmView", "group"),
isPersonal: equal("pmView", "user"),
group: null,
groupFilter: alias("group.name"),
currentPath: alias("router._router.currentPath"),
@ -22,56 +22,17 @@ export default Controller.extend({
showNewPM: and("user.viewingSelf", "currentUser.can_send_private_messages"),
@discourseComputed("inboxes", "isAllInbox")
displayGlobalFilters(inboxes, isAllInbox) {
if (inboxes.length === 0) {
return true;
}
if (inboxes.length && isAllInbox) {
return true;
}
return false;
},
@discourseComputed("inboxes")
sectionClass(inboxes) {
const defaultClass = "user-secondary-navigation user-messages";
return inboxes.length
? `${defaultClass} user-messages-inboxes`
: defaultClass;
},
@discourseComputed("pmView")
isPersonalInbox(pmView) {
return pmView && pmView.startsWith("user");
},
@discourseComputed("isPersonalInbox", "group.name")
isAllInbox(isPersonalInbox, groupName) {
return !this.isPersonalInbox && !groupName;
},
@discourseComputed("isPersonalInbox", "group.name")
selectedInbox(isPersonalInbox, groupName) {
if (groupName) {
return groupName;
}
return isPersonalInbox ? PERSONAL_INBOX : ALL_INBOX;
},
@discourseComputed("viewingSelf", "pmView", "currentUser.admin")
showWarningsWarning(viewingSelf, pmView, isAdmin) {
return pmView === VIEW_NAME_WARNINGS && !viewingSelf && !isAdmin;
},
@discourseComputed("pmTopicTrackingState.newIncoming.[]", "selectedInbox")
@discourseComputed("pmTopicTrackingState.newIncoming.[]", "group")
newLinkText() {
return this._linkText("new");
},
@discourseComputed("selectedInbox", "pmTopicTrackingState.newIncoming.[]")
@discourseComputed("pmTopicTrackingState.newIncoming.[]", "group")
unreadLinkText() {
return this._linkText("unread");
},
@ -86,45 +47,8 @@ export default Controller.extend({
}
},
@discourseComputed("model.groupsWithMessages")
inboxes(groupsWithMessages) {
if (!groupsWithMessages || groupsWithMessages.length === 0) {
return [];
}
const inboxes = [];
inboxes.push({
id: ALL_INBOX,
name: I18n.t("user.messages.all"),
});
inboxes.push({
id: PERSONAL_INBOX,
name: I18n.t("user.messages.personal"),
icon: "envelope",
});
groupsWithMessages.forEach((group) => {
inboxes.push({ id: group.name, name: group.name, icon: "users" });
});
return inboxes;
},
@action
changeGroupNotificationLevel(notificationLevel) {
this.group.setNotification(notificationLevel, this.get("user.model.id"));
},
@action
updateInbox(inbox) {
if (inbox === ALL_INBOX) {
this.transitionToRoute("userPrivateMessages.index");
} else if (inbox === PERSONAL_INBOX) {
this.transitionToRoute("userPrivateMessages.personal");
} else {
this.transitionToRoute("userPrivateMessages.group", inbox);
}
},
});

View File

@ -18,12 +18,11 @@ export default Controller.extend(BulkTopicSelection, {
showPosters: false,
channel: null,
tagsForUser: null,
pmTopicTrackingState: null,
incomingCount: reads("pmTopicTrackingState.newIncoming.length"),
@discourseComputed("emptyState", "model.topics.length", "incomingCount")
showEmptyStatePlaceholder(emptyState, topicsLength, incomingCount) {
return emptyState && topicsLength === 0 && incomingCount === 0;
@discourseComputed("model.topics.length", "incomingCount")
noContent(topicsLength, incomingCount) {
return topicsLength === 0 && incomingCount === 0;
},
saveScrollPosition() {
@ -46,15 +45,11 @@ export default Controller.extend(BulkTopicSelection, {
},
subscribe() {
this.pmTopicTrackingState?.trackIncoming(
this.inbox,
this.filter,
this.group
);
this.pmTopicTrackingState.trackIncoming(this.inbox, this.filter);
},
unsubscribe() {
this.pmTopicTrackingState?.resetTracking();
this.pmTopicTrackingState.stopIncomingTracking();
},
@action
@ -72,8 +67,11 @@ export default Controller.extend(BulkTopicSelection, {
opts.groupName = this.group.name;
}
Topic.pmResetNew(opts).then(() => {
this.send("refresh");
Topic.pmResetNew(opts).then((result) => {
if (result && result.topic_ids.length > 0) {
this.pmTopicTrackingState.removeTopics(result.topic_ids);
this.send("refresh");
}
});
},
@ -85,7 +83,7 @@ export default Controller.extend(BulkTopicSelection, {
@action
showInserted() {
this.model.loadBefore(this.pmTopicTrackingState.newIncoming);
this.pmTopicTrackingState.resetTracking();
this.pmTopicTrackingState.resetIncomingTracking();
return false;
},
});

View File

@ -61,6 +61,9 @@ export default Controller.extend(CanCheckEmails, {
isExpanded: !this.collapsedInfo,
icon: this.collapsedInfo ? "angle-double-down" : "angle-double-up",
label: this.collapsedInfo ? "expand_profile" : "collapse_profile",
ariaLabel: this.collapsedInfo
? "user.sr_expand_profile"
: "user.sr_collapse_profile",
action: this.collapsedInfo ? "expandProfile" : "collapseProfile",
};
}),

View File

@ -3,7 +3,7 @@ import Group from "discourse/models/group";
import { action } from "@ember/object";
import discourseDebounce from "discourse-common/lib/debounce";
import showModal from "discourse/lib/show-modal";
import { equal } from "@ember/object/computed";
import { and, equal } from "@ember/object/computed";
import { longDate } from "discourse/lib/formatter";
import { observes } from "discourse-common/utils/decorators";
@ -19,8 +19,9 @@ export default Controller.extend({
exclude_usernames: null,
isLoading: false,
columns: null,
groupsOptions: null,
groupOptions: null,
params: null,
showGroupFilter: and("currentUser", "groupOptions"),
showTimeRead: equal("period", "all"),
@ -66,15 +67,17 @@ export default Controller.extend({
},
loadGroups() {
return Group.findAll({ ignore_automatic: true }).then((groups) => {
const groupOptions = groups.map((group) => {
return {
name: group.full_name || group.name,
id: group.name,
};
if (this.currentUser) {
return Group.findAll({ ignore_automatic: true }).then((groups) => {
const groupOptions = groups.map((group) => {
return {
name: group.full_name || group.name,
id: group.name,
};
});
this.set("groupOptions", groupOptions);
});
this.set("groupOptions", groupOptions);
});
}
},
@action

View File

@ -17,7 +17,7 @@ registerUnbound("topic-link", (topic, args) => {
return htmlSafe(
`<a href='${url}'
role='heading'
level='2'
aria-level='2'
class='${classes.join(" ")}'
data-topic-id='${topic.id}'>${title}</a>`
);

View File

@ -6,14 +6,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Discourse - Ember CLI</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover">
<bootstrap-content key="before-script-load">
{{content-for "before-script-load"}}
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/discourse.js"></script>
<script src="{{rootURL}}assets/admin.js"></script>
<bootstrap-content key="head">
{{content-for "head"}}

View File

@ -0,0 +1,15 @@
import TextField from "@ember/component/text-field";
import TextArea from "@ember/component/text-area";
export default {
name: "ember-input-component-extensions",
initialize() {
TextField.reopen({
attributeBindings: ["aria-describedby", "aria-invalid"],
});
TextArea.reopen({
attributeBindings: ["aria-describedby", "aria-invalid"],
});
},
};

View File

@ -1,15 +1,11 @@
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import Mousetrap from "mousetrap";
import bindGlobal from "mousetrap-global-bind";
import ItsATrap from "@discourse/itsatrap";
export default {
name: "keyboard-shortcuts",
initialize(container) {
// Ensure mousetrap-global-bind is executed
void bindGlobal;
KeyboardShortcuts.init(Mousetrap, container);
KeyboardShortcuts.init(ItsATrap, container);
KeyboardShortcuts.bindEvents();
},

View File

@ -15,8 +15,8 @@ export default {
const router = container.lookup("router:main");
router.on("routeWillChange", viewTrackingRequired);
router.on("routeDidChange", () => {
cleanDOM(container);
router.on("routeDidChange", (transition) => {
cleanDOM(container, { skipMiniProfilerPageTransition: !transition.from });
});
let appEvents = container.lookup("service:app-events");

View File

@ -1,4 +1,8 @@
import { addComposerUploadProcessor } from "discourse/components/composer-editor";
import {
addComposerUploadPreProcessor,
addComposerUploadProcessor,
} from "discourse/components/composer-editor";
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
export default {
name: "register-media-optimization-upload-processor",
@ -6,15 +10,30 @@ export default {
initialize(container) {
let siteSettings = container.lookup("site-settings:main");
if (siteSettings.composer_media_optimization_image_enabled) {
addComposerUploadProcessor(
{ action: "optimizeJPEG" },
{
optimizeJPEG: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
}
);
if (!siteSettings.enable_experimental_composer_uploader) {
addComposerUploadProcessor(
{ action: "optimizeJPEG" },
{
optimizeJPEG: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
}
);
} else {
addComposerUploadPreProcessor(
UppyMediaOptimization,
({ isMobileDevice }) => {
return {
optimizeFn: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
runParallel: !isMobileDevice,
};
}
);
}
}
},
};

View File

@ -1,50 +1,10 @@
import getAbsoluteURL, { isAbsoluteURL } from "discourse-common/lib/get-url";
import { registerServiceWorker } from "discourse/lib/register-service-worker";
export default {
name: "register-service-worker",
initialize(container) {
const isSecured = document.location.protocol === "https:";
if (isSecured && "serviceWorker" in navigator) {
let { serviceWorkerURL } = container.lookup("session:main");
const caps = container.lookup("capabilities:main");
const isAppleBrowser =
caps.isSafari ||
(caps.isIOS &&
!window.matchMedia("(display-mode: standalone)").matches);
if (serviceWorkerURL && !isAppleBrowser) {
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) {
if (
registration.active &&
!registration.active.scriptURL.includes(serviceWorkerURL)
) {
this.unregister(registration);
}
}
});
navigator.serviceWorker
.register(getAbsoluteURL(`/${serviceWorkerURL}`))
.catch((error) => {
// eslint-disable-next-line no-console
console.info(`Failed to register Service Worker: ${error}`);
});
} else {
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) {
this.unregister(registration);
}
});
}
}
},
unregister(registration) {
if (isAbsoluteURL(registration.scope)) {
registration.unregister();
}
let { serviceWorkerURL } = container.lookup("session:main");
registerServiceWorker(container, serviceWorkerURL);
},
};

View File

@ -67,8 +67,7 @@ export default {
dependentKeys: ["topic.bookmarked", "topic.bookmarksWereChanged"],
id: "bookmark",
icon() {
const bookmarkedPosts = this.topic.bookmarked_posts;
if (bookmarkedPosts && bookmarkedPosts.find((x) => x.reminder_at)) {
if (this.topic.bookmarks.some((bookmark) => bookmark.reminder_at)) {
return "discourse-bookmark-clock";
}
return "bookmark";
@ -81,14 +80,9 @@ export default {
},
label() {
if (!this.topic.isPrivateMessage || this.site.mobileView) {
const bookmarkedPosts = this.topic.bookmarked_posts;
const bookmarkedPostsCount = bookmarkedPosts
? bookmarkedPosts.length
: 0;
if (bookmarkedPostsCount === 0) {
if (this.topic.bookmarkCount === 0) {
return "bookmarked.title";
} else if (bookmarkedPostsCount === 1) {
} else if (this.topic.bookmarkCount === 1) {
return "bookmarked.edit_bookmark";
} else {
return "bookmarked.clear_bookmarks";
@ -96,12 +90,19 @@ export default {
}
},
translatedTitle() {
const bookmarkedPosts = this.topic.bookmarked_posts;
if (!bookmarkedPosts || bookmarkedPosts.length === 0) {
if (this.topic.bookmarkCount === 0) {
return I18n.t("bookmarked.help.bookmark");
} else if (bookmarkedPosts.length === 1) {
return I18n.t("bookmarked.help.edit_bookmark");
} else if (bookmarkedPosts.find((x) => x.reminder_at)) {
} else if (this.topic.bookmarkCount === 1) {
if (
this.topic.bookmarks.filter((bookmark) => bookmark.for_topic).length
) {
return I18n.t("bookmarked.help.edit_bookmark_for_topic");
} else {
return I18n.t("bookmarked.help.edit_bookmark");
}
} else if (
this.topic.bookmarks.some((bookmark) => bookmark.reminder_at)
) {
return I18n.t("bookmarked.help.unbookmark_with_reminder");
} else {
return I18n.t("bookmarked.help.unbookmark");

View File

@ -20,12 +20,11 @@ function updateCache(term, results) {
function searchTags(term, categories, limit) {
return new Promise((resolve) => {
const clearPromise = later(
() => {
resolve(CANCELLED_STATUS);
},
isTesting() ? 50 : 5000
);
let clearPromise = isTesting()
? null
: later(() => {
resolve(CANCELLED_STATUS);
}, 5000);
const debouncedSearch = (q, cats, resultFunc) => {
discourseDebounce(

View File

@ -1,7 +1,7 @@
import { scheduleOnce } from "@ember/runloop";
function _clean() {
if (window.MiniProfiler) {
function _clean(opts = {}) {
if (window.MiniProfiler && !opts.skipMiniProfilerPageTransition) {
window.MiniProfiler.pageTransition();
}
@ -29,6 +29,6 @@ function _clean() {
this.lookup("service:document-title").updateContextCount(0);
}
export function cleanDOM(container) {
scheduleOnce("afterRender", container, _clean);
export function cleanDOM(container, opts) {
scheduleOnce("afterRender", container, _clean, opts);
}

View File

@ -128,7 +128,7 @@ function setupNotifications(appEvents) {
appEvents.on("page:changed", resetIdle);
}
function resetIdle() {
export function resetIdle() {
lastAction = Date.now();
}
function isIdle() {
@ -153,11 +153,13 @@ function onNotification(data, siteSettings, user) {
return;
}
const notificationTitle = I18n.t(i18nKey(data.notification_type), {
site_title: siteSettings.title,
topic: data.topic_title,
username: formatUsername(data.username),
});
const notificationTitle =
data.translated_title ||
I18n.t(i18nKey(data.notification_type), {
site_title: siteSettings.title,
topic: data.topic_title,
username: formatUsername(data.username),
});
const notificationBody = data.excerpt;

View File

@ -0,0 +1,90 @@
import User from "discourse/models/user";
import showModal from "discourse/lib/show-modal";
import getURL from "discourse-common/lib/get-url";
export function downloadCalendar(title, dates) {
const currentUser = User.current();
const formattedDates = formatDates(dates);
title = title.trim();
switch (currentUser.default_calendar) {
case "none_selected":
_displayModal(title, formattedDates);
break;
case "ics":
downloadIcs(title, formattedDates);
break;
case "google":
downloadGoogle(title, formattedDates);
break;
}
}
export function downloadIcs(title, dates) {
const REMOVE_FILE_AFTER = 20_000;
const file = new File([generateIcsData(title, dates)], {
type: "text/plain",
});
const a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = window.URL.createObjectURL(file);
a.download = `${title.toLowerCase().replace(/[^\w]/g, "-")}.ics`;
a.click();
setTimeout(() => window.URL.revokeObjectURL(file), REMOVE_FILE_AFTER); //remove file to avoid memory leaks
}
export function downloadGoogle(title, dates) {
dates.forEach((date) => {
const encodedTitle = encodeURIComponent(title);
const link = getURL(`
https://www.google.com/calendar/event?action=TEMPLATE&text=${encodedTitle}&dates=${_formatDateForGoogleApi(
date.startsAt
)}/${_formatDateForGoogleApi(date.endsAt)}
`).trim();
window.open(link, "_blank", "noopener", "noreferrer");
});
}
export function formatDates(dates) {
return dates.map((date) => {
return {
startsAt: date.startsAt,
endsAt: date.endsAt
? date.endsAt
: moment.utc(date.startsAt).add(1, "hours").format(),
};
});
}
export function generateIcsData(title, dates) {
let data = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Discourse//EN\n";
dates.forEach((date) => {
const startDate = moment(date.startsAt);
const endDate = moment(date.endsAt);
data = data.concat(
"BEGIN:VEVENT\n" +
`UID:${startDate.utc().format("x")}_${endDate.format("x")}\n` +
`DTSTAMP:${moment().utc().format("YMMDDTHHmmss")}Z\n` +
`DTSTART:${startDate.utc().format("YMMDDTHHmmss")}Z\n` +
`DTEND:${endDate.utc().format("YMMDDTHHmmss")}Z\n` +
`SUMMARY:${title}\n` +
"END:VEVENT\n"
);
});
data = data.concat("END:VCALENDAR");
return data;
}
function _displayModal(title, dates) {
showModal("download-calendar", { model: { title, dates } });
}
function _formatDateForGoogleApi(date) {
return moment(date)
.toISOString()
.replace(/-|:|\.\d\d\d/g, "");
}

Some files were not shown because too many files have changed in this diff Show More