Version bump
This commit is contained in:
commit
414d39f883
14
.github/workflows/ember.yml
vendored
14
.github/workflows/ember.yml
vendored
@ -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
|
||||
|
||||
49
.github/workflows/ember_with_plugins.yml
vendored
Normal file
49
.github/workflows/ember_with_plugins.yml
vendored
Normal 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
|
||||
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
||||
3
Gemfile
3
Gemfile
@ -210,6 +210,9 @@ gem 'gc_tracer', require: false, platform: :mri
|
||||
# required for feed importing and embedding
|
||||
gem 'ruby-readability', require: false
|
||||
|
||||
# rss gem is a bundled gem from Ruby 3 onwards
|
||||
gem 'rss', require: false
|
||||
|
||||
gem 'stackprof', require: false, platform: :mri
|
||||
gem 'memory_profiler', require: false, platform: :mri
|
||||
|
||||
|
||||
66
Gemfile.lock
66
Gemfile.lock
@ -53,8 +53,8 @@ GEM
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.432.0)
|
||||
aws-sdk-core (3.112.1)
|
||||
aws-partitions (1.516.0)
|
||||
aws-sdk-core (3.121.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
@ -66,10 +66,10 @@ GEM
|
||||
aws-sdk-core (~> 3, >= 3.112.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-sns (1.38.0)
|
||||
aws-sdk-core (~> 3, >= 3.112.0)
|
||||
aws-sdk-sns (1.46.0)
|
||||
aws-sdk-core (~> 3, >= 3.121.2)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.3)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
barber (0.12.2)
|
||||
ember-source (>= 1.0, < 3.1)
|
||||
@ -80,7 +80,7 @@ GEM
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.8.1)
|
||||
bootsnap (1.9.1)
|
||||
msgpack (~> 1.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.5)
|
||||
@ -129,14 +129,14 @@ GEM
|
||||
sprockets (>= 3.3, < 4.1)
|
||||
ember-source (2.18.2)
|
||||
erubi (1.10.0)
|
||||
excon (0.85.0)
|
||||
excon (0.87.0)
|
||||
execjs (2.8.1)
|
||||
exifr (1.3.9)
|
||||
fabrication (2.22.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
fakeweb (1.3.0)
|
||||
faraday (1.7.1)
|
||||
faraday (1.8.0)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@ -173,7 +173,7 @@ GEM
|
||||
http_accept_language (2.1.1)
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_optim (0.30.0)
|
||||
image_optim (0.31.0)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
fspath (~> 3.0)
|
||||
image_size (>= 1.5, < 3)
|
||||
@ -181,12 +181,13 @@ GEM
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
image_size (2.1.2)
|
||||
in_threads (1.5.4)
|
||||
ipaddr (1.2.2)
|
||||
jmespath (1.4.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.5.1)
|
||||
json (2.6.0)
|
||||
json-schema (2.8.1)
|
||||
addressable (>= 2.4)
|
||||
json_schemer (0.2.18)
|
||||
@ -194,7 +195,7 @@ GEM
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.2.3)
|
||||
jwt (2.3.0)
|
||||
kgio (2.11.4)
|
||||
libv8-node (15.14.0.1)
|
||||
libv8-node (15.14.0.1-arm64-darwin-20)
|
||||
@ -225,7 +226,7 @@ GEM
|
||||
message_bus (3.3.6)
|
||||
rack (>= 1.1.3)
|
||||
method_source (1.0.0)
|
||||
mini_mime (1.1.1)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.6.1)
|
||||
mini_racer (0.4.0)
|
||||
libv8-node (~> 15.14.0.0)
|
||||
@ -244,14 +245,14 @@ GEM
|
||||
multipart-post (2.1.1)
|
||||
mustache (1.1.1)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.12.4)
|
||||
nokogiri (1.12.5)
|
||||
mini_portile2 (~> 2.6.1)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.12.4-arm64-darwin)
|
||||
nokogiri (1.12.5-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.12.4-x86_64-darwin)
|
||||
nokogiri (1.12.5-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.12.4-x86_64-linux)
|
||||
nokogiri (1.12.5-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
oauth2 (1.4.7)
|
||||
@ -283,12 +284,13 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
openssl (2.2.0)
|
||||
openssl (2.2.1)
|
||||
ipaddr
|
||||
openssl-signature_algorithm (1.1.1)
|
||||
openssl (~> 2.0)
|
||||
optimist (3.0.1)
|
||||
parallel (1.20.1)
|
||||
parallel_tests (3.7.1)
|
||||
parallel (1.21.0)
|
||||
parallel_tests (3.7.3)
|
||||
parallel
|
||||
parser (3.0.2.0)
|
||||
ast (~> 2.4.1)
|
||||
@ -303,10 +305,10 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.4.0)
|
||||
puma (5.5.2)
|
||||
nio4r (~> 2.0)
|
||||
r2 (0.2.7)
|
||||
racc (1.5.2)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
rack-mini-profiler (2.3.3)
|
||||
rack (>= 1.2.0)
|
||||
@ -323,7 +325,7 @@ GEM
|
||||
activerecord (~> 6.0)
|
||||
concurrent-ruby
|
||||
railties (~> 6.0)
|
||||
rails_multisite (3.0.0)
|
||||
rails_multisite (3.1.0)
|
||||
activerecord (> 5.0, < 7)
|
||||
railties (> 5.0, < 7)
|
||||
railties (6.1.4.1)
|
||||
@ -343,7 +345,7 @@ GEM
|
||||
msgpack (>= 0.4.3)
|
||||
optimist (>= 3.0.0)
|
||||
rchardet (1.8.0)
|
||||
redis (4.4.0)
|
||||
redis (4.5.1)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
@ -380,28 +382,29 @@ GEM
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rss (0.2.9)
|
||||
rexml
|
||||
rswag-specs (2.4.0)
|
||||
activesupport (>= 3.1, < 7.0)
|
||||
json-schema (~> 2.2)
|
||||
railties (>= 3.1, < 7.0)
|
||||
rtlit (0.0.5)
|
||||
rubocop (1.20.0)
|
||||
rubocop (1.22.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.9.1, < 2.0)
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.11.0)
|
||||
rubocop-ast (1.12.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-discourse (2.4.2)
|
||||
rubocop (>= 1.1.0)
|
||||
rubocop-rspec (>= 2.0.0)
|
||||
rubocop-rspec (2.4.0)
|
||||
rubocop (~> 1.0)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
rubocop-rspec (2.5.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-prof (1.4.3)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-readability (0.7.0)
|
||||
@ -454,8 +457,8 @@ GEM
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (2.0.0)
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.1.0)
|
||||
unicorn (6.0.0)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
@ -581,6 +584,7 @@ DEPENDENCIES
|
||||
rspec
|
||||
rspec-html-matchers
|
||||
rspec-rails
|
||||
rss
|
||||
rswag-specs
|
||||
rtlit
|
||||
rubocop-discourse
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import ImageUploader from "discourse/components/image-uploader";
|
||||
|
||||
export default ImageUploader.extend({
|
||||
layoutName: "components/image-uploader",
|
||||
uploadUrlParams: "&for_site_setting=true",
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
{{text-field value=(html-safe value) classNames="input-setting-string"}}
|
||||
<div class="desc">{{html-safe setting.description}}</div>
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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__) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
@ -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({});
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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");
|
||||
},
|
||||
|
||||
@ -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+"),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -40,4 +40,12 @@ export default Component.extend({
|
||||
? "discourse-compress"
|
||||
: "discourse-expand";
|
||||
},
|
||||
|
||||
@discourseComputed("disableTextarea")
|
||||
showFullScreenButton(disableTextarea) {
|
||||
if (this.site.mobileView) {
|
||||
return false;
|
||||
}
|
||||
return !disableTextarea;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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 || "";
|
||||
},
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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], "");
|
||||
|
||||
@ -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",
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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>`;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@ -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();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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();
|
||||
},
|
||||
},
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
@ -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) => {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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));
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -428,7 +428,10 @@ export default Controller.extend(ModalFunctionality, {
|
||||
});
|
||||
|
||||
next(() => {
|
||||
showModal("createAccount", { modalClass: "create-account" });
|
||||
showModal("createAccount", {
|
||||
modalClass: "create-account",
|
||||
titleAriaElementId: "create-account-title",
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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",
|
||||
};
|
||||
}),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>`
|
||||
);
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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"],
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
Reference in New Issue
Block a user