Version bump

This commit is contained in:
Neil Lalonde 2021-06-08 11:30:09 -04:00
commit d0e09c512c
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
3968 changed files with 96664 additions and 7386 deletions

View File

@ -1 +1 @@
<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in Javascript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->

View File

@ -111,7 +111,7 @@ jobs:
- name: Core RSpec
if: matrix.build_type == 'backend' && matrix.target == 'core'
run: bin/turbo_rspec
run: bin/turbo_rspec --verbose
- name: Plugin RSpec
if: matrix.build_type == 'backend' && matrix.target == 'plugins'

59
COPYRIGHT.md Normal file
View File

@ -0,0 +1,59 @@
# Legal notice
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with this program as the file LICENSE.txt; if not, please see
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
## Trademark
Discourse is a registered trademark of Civilized Discourse Construction Kit.
## Other copyright notices
Discourse includes works under other copyright notices and distributed
according to the terms of the GNU General Public License or a compatible
license (where indicated), including:
- Ember.js - Copyright (c) 2020 Yehuda Katz, Tom Dale and Ember.js contributors
MIT License
- jQuery - Copyright OpenJS Foundation and other contributors, https://openjsf.org/
MIT License
- Rails - Copyright (c) 2005-2021 David Heinemeier Hansson
MIT License
- Onebox - Copyright (c) 2013 jzeta
MIT License
MIT License:
```
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

View File

@ -1,31 +0,0 @@
All Discourse code is Copyright 2013 by Civilized Discourse Construction Kit, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with this program as the file LICENSE.txt; if not, please see
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
Discourse is a registered trademark of Civilized Discourse Construction Kit.
Discourse includes works under other copyright notices and distributed
according to the terms of the GNU General Public License or a compatible
license (where indicated), including:
Javascript
Ember.js - Copyright (c) 2012-2013 Yehuda Katz, Tom Dale, Charles Jolley and Ember.js contributors
jQuery - Copyright (c) 2010-2013 John Resig
Ruby
Rails - Copyright (c) 2005-2013 David Heinemeier Hansson, Rails Core Team contributors (MIT)

View File

@ -60,8 +60,6 @@ gem 'redis-namespace'
# better maintained living fork
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox'
gem 'http_accept_language', require: false
# Ember related gems need to be pinned cause they control client side
@ -90,9 +88,7 @@ gem 'unf', require: false
gem 'email_reply_trimmer'
# Forked until https://github.com/toy/image_optim/pull/162 is merged
# https://github.com/discourse/image_optim
gem 'discourse_image_optim', require: 'image_optim'
gem 'image_optim'
gem 'multi_json'
gem 'mustache'
gem 'nokogiri'
@ -231,6 +227,8 @@ gem 'sshkey', require: false
gem 'rchardet', require: false
gem 'lz4-ruby', require: false, platform: :ruby
gem 'sanitize'
if ENV["IMPORT"] == "1"
gem 'mysql2'
gem 'redcarpet'

View File

@ -92,7 +92,7 @@ GEM
chunky_png (1.4.0)
coderay (1.1.3)
colored2 (3.1.2)
concurrent-ruby (1.1.8)
concurrent-ruby (1.1.9)
connection_pool (2.2.5)
cose (1.2.0)
cbor (~> 0.5.9)
@ -117,12 +117,6 @@ GEM
discourse-fonts (0.0.8)
discourse_dev (0.2.1)
faker (~> 2.16)
discourse_image_optim (0.26.2)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
docile (1.4.0)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
@ -134,26 +128,30 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.10.0)
excon (0.81.0)
excon (0.82.0)
execjs (2.8.1)
exifr (1.3.9)
fabrication (2.22.0)
faker (2.17.0)
faker (2.18.0)
i18n (>= 1.6, < 2)
fakeweb (1.3.0)
faraday (1.4.1)
faraday (1.4.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.1.0)
fast_blank (1.0.0)
fast_xs (0.8.0)
fastimage (2.2.3)
ffi (1.15.0)
fastimage (2.2.4)
ffi (1.15.1)
fspath (3.1.2)
gc_tracer (1.5.1)
globalid (0.4.2)
@ -168,7 +166,13 @@ GEM
http_accept_language (2.1.1)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
image_size (1.5.0)
image_optim (0.30.0)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (>= 1.5, < 3)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
image_size (2.1.0)
in_threads (1.5.4)
jmespath (1.4.0)
jquery-rails (4.4.0)
@ -184,7 +188,7 @@ GEM
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.2.3)
kgio (2.11.3)
kgio (2.11.4)
libv8-node (15.14.0.1)
libv8-node (15.14.0.1-arm64-darwin-20)
libv8-node (15.14.0.1-x86_64-darwin-18)
@ -203,18 +207,18 @@ GEM
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.9.6)
loofah (2.9.1)
loofah (2.10.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
lz4-ruby (0.3.3)
maxminddb (0.1.22)
memory_profiler (1.0.0)
message_bus (3.3.5)
message_bus (3.3.6)
rack (>= 1.1.3)
method_source (1.0.0)
mini_mime (1.1.0)
mini_portile2 (2.5.1)
mini_portile2 (2.5.3)
mini_racer (0.4.0)
libv8-node (~> 15.14.0.0)
mini_scheduler (0.13.0)
@ -232,14 +236,14 @@ GEM
multipart-post (2.1.1)
mustache (1.1.1)
nio4r (2.5.7)
nokogiri (1.11.3)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogiri (1.11.3-arm64-darwin)
nokogiri (1.11.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.11.3-x86_64-darwin)
nokogiri (1.11.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.11.3-x86_64-linux)
nokogiri (1.11.7-x86_64-linux)
racc (~> 1.4)
nokogumbo (2.0.5)
nokogiri (~> 1.8, >= 1.8.4)
@ -273,13 +277,6 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (2.2.15)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
openssl (2.2.0)
openssl-signature_algorithm (1.1.1)
openssl (~> 2.0)
@ -300,7 +297,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.3.1)
puma (5.3.2)
nio4r (~> 2.0)
r2 (0.2.7)
racc (1.5.2)
@ -330,7 +327,7 @@ GEM
rake (>= 0.8.7)
thor (~> 1.0)
rainbow (3.0.0)
raindrops (0.19.1)
raindrops (0.19.2)
rake (13.0.3)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
@ -382,18 +379,18 @@ GEM
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (1.14.0)
rubocop (1.16.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.5.0, < 2.0)
rubocop-ast (>= 1.7.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.5.0)
rubocop-ast (1.7.0)
parser (>= 3.0.1.1)
rubocop-discourse (2.4.1)
rubocop-discourse (2.4.2)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.3.0)
@ -459,7 +456,7 @@ GEM
raindrops (~> 0.7)
uniform_notifier (1.14.2)
uri_template (0.7.0)
webmock (3.12.2)
webmock (3.13.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -508,7 +505,6 @@ DEPENDENCIES
discourse-ember-source (~> 3.12.2)
discourse-fonts
discourse_dev
discourse_image_optim
email_reply_trimmer
ember-handlebars-template (= 0.8.0)
excon
@ -522,6 +518,7 @@ DEPENDENCIES
highline
htmlentities
http_accept_language
image_optim
json
json_schemer
listen
@ -554,7 +551,6 @@ DEPENDENCIES
omniauth-google-oauth2
omniauth-oauth2
omniauth-twitter
onebox
parallel_tests
pg
pry-byebug
@ -585,6 +581,7 @@ DEPENDENCIES
ruby-prof
ruby-readability
rubyzip
sanitize
sassc (= 2.0.1)
sassc-rails
seed-fu
@ -606,4 +603,4 @@ DEPENDENCIES
yaml-lint
BUNDLED WITH
2.2.16
2.2.19

View File

@ -1,12 +1,12 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
@ -56,7 +56,7 @@ patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
@ -277,9 +277,9 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it

View File

@ -366,7 +366,7 @@ export default Component.extend({
},
_buildPayload(facets) {
let payload = { data: { cache: true, facets } };
let payload = { data: { facets } };
if (this.startDate) {
payload.data.start_date = moment(this.startDate)

View File

@ -1,10 +1,21 @@
import Component from "@ember/component";
import I18n from "I18n";
import { equal } from "@ember/object/computed";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Component.extend({
classNames: ["watched-word"],
isReplace: equal("actionKey", "replace"),
isTag: equal("actionKey", "tag"),
isLink: equal("actionKey", "link"),
@discourseComputed("word.replacement")
tags(replacement) {
return replacement.split(",");
},
click() {
this.word
.destroy()

View File

@ -13,7 +13,7 @@ export default Component.extend({
reasonKeys: [
"not_listening_to_staff",
"consuming_staff_time",
"combatative",
"combative",
"in_wrong_place",
"no_constructive_purpose",
CUSTOM_REASON_KEY,

View File

@ -15,16 +15,24 @@ export default Component.extend({
formSubmitted: false,
actionKey: null,
showMessage: false,
selectedTags: null,
canReplace: equal("actionKey", "replace"),
canTag: equal("actionKey", "tag"),
canLink: equal("actionKey", "link"),
@discourseComputed("regularExpressions")
placeholderKey(regularExpressions) {
return (
"admin.watched_words.form.placeholder" +
(regularExpressions ? "_regexp" : "")
);
didInsertElement() {
this._super(...arguments);
this.set("selectedTags", []);
},
@discourseComputed("siteSettings.watched_words_regular_expressions")
placeholderKey(watchedWordsRegularExpressions) {
if (watchedWordsRegularExpressions) {
return "admin.watched_words.form.placeholder_regexp";
} else {
return "admin.watched_words.form.placeholder";
}
},
@observes("word")
@ -46,6 +54,13 @@ export default Component.extend({
},
actions: {
changeSelectedTags(tags) {
this.setProperties({
selectedTags: tags,
replacement: tags.join(","),
});
},
submit() {
if (!this.isUniqueWord) {
this.setProperties({
@ -60,7 +75,10 @@ export default Component.extend({
const watchedWord = WatchedWord.create({
word: this.word,
replacement: this.canReplace || this.canTag ? this.replacement : null,
replacement:
this.canReplace || this.canTag || this.canLink
? this.replacement
: null,
action: this.actionKey,
});

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import AdminDashboard from "admin/models/admin-dashboard";
import I18n from "I18n";
import PeriodComputationMixin from "admin/mixins/period-computation";
@ -18,7 +18,7 @@ function staticReport(reportType) {
export default Controller.extend(PeriodComputationMixin, {
isLoading: false,
dashboardFetchedAt: null,
exceptionController: inject("exception"),
exceptionController: controller("exception"),
logSearchQueriesEnabled: setting("log_search_queries"),
@discourseComputed("siteSettings.dashboard_general_tab_activity_metrics")

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import AdminDashboard from "admin/models/admin-dashboard";
import VersionCheck from "admin/models/version-check";
import { computed } from "@ember/object";
@ -10,7 +10,7 @@ const PROBLEMS_CHECK_MINUTES = 1;
export default Controller.extend({
isLoading: false,
dashboardFetchedAt: null,
exceptionController: inject("exception"),
exceptionController: controller("exception"),
showVersionChecks: setting("version_checks"),
@discourseComputed("problems.length")

View File

@ -12,20 +12,14 @@ import showModal from "discourse/lib/show-modal";
export default Controller.extend({
adminWatchedWords: controller(),
actionNameKey: null,
showWordsList: or(
"adminWatchedWords.filtered",
"adminWatchedWords.showWords"
),
downloadLink: fmt(
"actionNameKey",
"/admin/customize/watched_words/action/%@/download"
),
showWordsList: or("adminWatchedWords.showWords", "adminWatchedWords.filter"),
findAction(actionName) {
return (this.get("adminWatchedWords.model") || []).findBy(
"nameKey",
actionName
);
return (this.adminWatchedWords.model || []).findBy("nameKey", actionName);
},
@discourseComputed("actionNameKey", "adminWatchedWords.model")
@ -33,9 +27,15 @@ export default Controller.extend({
return this.findAction(actionName);
},
@discourseComputed("currentAction.words.[]", "adminWatchedWords.model")
filteredContent(words) {
return words || [];
@discourseComputed("currentAction.words.[]")
regexpError(words) {
for (const { regexp, word } of words) {
try {
RegExp(regexp);
} catch {
return I18n.t("admin.watched_words.invalid_regex", { word });
}
}
},
@discourseComputed("actionNameKey")
@ -43,47 +43,51 @@ export default Controller.extend({
return I18n.t("admin.watched_words.action_descriptions." + actionNameKey);
},
@discourseComputed("currentAction.count")
wordCount(count) {
return count || 0;
},
actions: {
recordAdded(arg) {
const a = this.findAction(this.actionNameKey);
if (a) {
a.words.unshiftObject(arg);
a.incrementProperty("count");
schedule("afterRender", () => {
// remove from other actions lists
let match = null;
this.get("adminWatchedWords.model").forEach((action) => {
if (match) {
return;
}
if (action.nameKey !== this.actionNameKey) {
match = action.words.findBy("id", arg.id);
if (match) {
action.words.removeObject(match);
action.decrementProperty("count");
}
}
});
});
const action = this.findAction(this.actionNameKey);
if (!action) {
return;
}
action.words.unshiftObject(arg);
schedule("afterRender", () => {
// remove from other actions lists
let match = null;
this.adminWatchedWords.model.forEach((otherAction) => {
if (match) {
return;
}
if (otherAction.nameKey !== this.actionNameKey) {
match = otherAction.words.findBy("id", arg.id);
if (match) {
otherAction.words.removeObject(match);
}
}
});
});
},
recordRemoved(arg) {
if (this.currentAction) {
this.currentAction.words.removeObject(arg);
this.currentAction.decrementProperty("count");
}
},
uploadComplete() {
WatchedWord.findAll().then((data) => {
this.set("adminWatchedWords.model", data);
this.adminWatchedWords.set("model", data);
});
},
test() {
WatchedWord.findAll().then((data) => {
this.adminWatchedWords.set("model", data);
showModal("admin-watched-word-test", {
admin: true,
model: this.currentAction,
});
});
},
@ -102,25 +106,12 @@ export default Controller.extend({
}).then(() => {
const action = this.findAction(actionKey);
if (action) {
action.setProperties({
words: [],
count: 0,
});
action.set("words", []);
}
});
}
}
);
},
test() {
WatchedWord.findAll().then((data) => {
this.set("adminWatchedWords.model", data);
showModal("admin-watched-word-test", {
admin: true,
model: this.currentAction,
});
});
},
},
});

View File

@ -1,71 +1,56 @@
import Controller from "@ember/controller";
import EmberObject from "@ember/object";
import EmberObject, { action } from "@ember/object";
import { INPUT_DELAY } from "discourse-common/config/environment";
import { alias } from "@ember/object/computed";
import discourseDebounce from "discourse-common/lib/debounce";
import { isEmpty } from "@ember/utils";
import { observes } from "discourse-common/utils/decorators";
export default Controller.extend({
filter: null,
filtered: false,
showWords: false,
disableShowWords: alias("filtered"),
regularExpressions: null,
filterContentNow() {
if (!!isEmpty(this.allWatchedWords)) {
_filterContent() {
if (isEmpty(this.allWatchedWords)) {
return;
}
let filter;
if (this.filter) {
filter = this.filter.toLowerCase();
}
if (filter === undefined || filter.length < 1) {
if (!this.filter) {
this.set("model", this.allWatchedWords);
return;
}
const matchesByAction = [];
const filter = this.filter.toLowerCase();
const model = [];
this.allWatchedWords.forEach((wordsForAction) => {
const wordRecords = wordsForAction.words.filter((wordRecord) => {
return wordRecord.word.indexOf(filter) > -1;
});
matchesByAction.pushObject(
model.pushObject(
EmberObject.create({
nameKey: wordsForAction.nameKey,
name: wordsForAction.name,
words: wordRecords,
count: wordRecords.length,
})
);
});
this.set("model", matchesByAction);
this.set("model", model);
},
@observes("filter")
filterContent() {
discourseDebounce(
this,
function () {
this.filterContentNow();
this.set("filtered", !isEmpty(this.filter));
},
INPUT_DELAY
);
discourseDebounce(this, this._filterContent, INPUT_DELAY);
},
actions: {
clearFilter() {
this.setProperties({ filter: "" });
},
@action
clearFilter() {
this.set("filter", "");
},
toggleMenu() {
$(".admin-detail").toggleClass("mobile-closed mobile-open");
},
@action
toggleMenu() {
$(".admin-detail").toggleClass("mobile-closed mobile-open");
},
});

View File

@ -1,14 +1,50 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
export default Controller.extend(ModalFunctionality, {
@discourseComputed("value", "model.compiledRegularExpression")
matches(value, regexpString) {
isReplace: equal("model.nameKey", "replace"),
isTag: equal("model.nameKey", "tag"),
isLink: equal("model.nameKey", "link"),
@discourseComputed(
"value",
"model.compiledRegularExpression",
"model.words",
"isReplace",
"isTag",
"isLink"
)
matches(value, regexpString, words, isReplace, isTag, isLink) {
if (!value || !regexpString) {
return;
}
let censorRegexp = new RegExp(regexpString, "ig");
return value.match(censorRegexp);
const regexp = new RegExp(regexpString, "ig");
const matches = value.match(regexp) || [];
if (isReplace || isLink) {
return matches.map((match) => ({
match,
replacement: words.find((word) =>
new RegExp(word.regexp, "ig").test(match)
).replacement,
}));
} else if (isTag) {
return matches.map((match) => {
const tags = new Set();
words.forEach((word) => {
if (new RegExp(word.regexp, "ig").test(match)) {
word.replacement.split(",").forEach((tag) => tags.add(tag));
}
});
return { match, tags: Array.from(tags) };
});
}
return matches;
},
});

View File

@ -31,27 +31,21 @@ WatchedWord.reopenClass({
findAll() {
return ajax("/admin/customize/watched_words.json").then((list) => {
const actions = {};
list.words.forEach((s) => {
if (!actions[s.action]) {
actions[s.action] = [];
}
actions[s.action].pushObject(WatchedWord.create(s));
list.actions.forEach((action) => {
actions[action] = [];
});
list.actions.forEach((a) => {
if (!actions[a]) {
actions[a] = [];
}
list.words.forEach((watchedWord) => {
actions[watchedWord.action].pushObject(WatchedWord.create(watchedWord));
});
return Object.keys(actions).map((n) => {
return Object.keys(actions).map((nameKey) => {
return EmberObject.create({
nameKey: n,
name: I18n.t("admin.watched_words.actions." + n),
words: actions[n],
count: actions[n].length,
regularExpressions: list.regular_expressions,
compiledRegularExpression: list.compiled_regular_expressions[n],
nameKey,
name: I18n.t("admin.watched_words.actions." + nameKey),
words: actions[nameKey],
compiledRegularExpression: list.compiled_regular_expressions[nameKey],
});
});
});

View File

@ -4,17 +4,12 @@ import I18n from "I18n";
export default DiscourseRoute.extend({
model(params) {
this.controllerFor("adminWatchedWordsAction").set(
"actionNameKey",
params.action_id
);
let filteredContent = this.controllerFor("adminWatchedWordsAction").get(
"filteredContent"
);
const controller = this.controllerFor("adminWatchedWordsAction");
controller.set("actionNameKey", params.action_id);
return EmberObject.create({
nameKey: params.action_id,
name: I18n.t("admin.watched_words.actions." + params.action_id),
words: filteredContent,
words: controller.filteredContent,
});
},
});

View File

@ -10,17 +10,8 @@ export default DiscourseRoute.extend({
return WatchedWord.findAll();
},
setupController(controller, model) {
controller.set("model", model);
if (model && model.length) {
controller.set("regularExpressions", model[0].get("regularExpressions"));
}
},
afterModel(watchedWordsList) {
this.controllerFor("adminWatchedWords").set(
"allWatchedWords",
watchedWordsList
);
afterModel(model) {
const controller = this.controllerFor("adminWatchedWords");
controller.set("allWatchedWords", model);
},
});

View File

@ -1 +1,9 @@
{{d-icon "times"}} {{word.word}} {{#if word.replacement}}&rarr; <span class="replacement">{{word.replacement}}</span>{{/if}}
{{d-icon "times"}} {{word.word}}
{{#if (or isReplace isLink)}}
&rarr; <span class="replacement">{{word.replacement}}</span>
{{else if isTag}}
&rarr;
{{#each tags as |tag|}}
<span class="tag">{{tag}}</span>
{{/each}}
{{/if}}

View File

@ -5,15 +5,29 @@
{{#if canReplace}}
<div class="watched-word-input">
<label for="watched-replacement">{{i18n "admin.watched_words.form.replacement_label"}}</label>
{{text-field id="watched-replacement" value=replacement disabled=formSubmitted class="watched-word-input-replace" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.replacement_placeholder"}}
<label for="watched-replacement">{{i18n "admin.watched_words.form.replace_label"}}</label>
{{text-field id="watched-replacement" value=replacement disabled=formSubmitted class="watched-word-input-field" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.replace_placeholder"}}
</div>
{{/if}}
{{#if canTag}}
<div class="watched-word-input">
<label for="watched-tag">{{i18n "admin.watched_words.form.tag_label"}}</label>
{{text-field id="watched-tag" value=replacement disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.tag_placeholder"}}
{{tag-chooser
id="watched-tag"
class="watched-word-input-field"
allowCreate=true
disabled=formSubmitted
tags=selectedTags
onChange=(action "changeSelectedTags")
}}
</div>
{{/if}}
{{#if canLink}}
<div class="watched-word-input">
<label for="watched-replacement">{{i18n "admin.watched_words.form.link_label"}}</label>
{{text-field id="watched-replacement" value=replacement disabled=formSubmitted class="watched-word-input-field" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.link_placeholder"}}
</div>
{{/if}}

View File

@ -1,4 +1,4 @@
{{#d-modal-body class="upload-selector install-theme" title="admin.customize.theme.install"}}
{{#d-modal-body class="install-theme" title="admin.customize.theme.install"}}
{{#unless directRepoInstall}}
<div class="install-theme-items">
{{install-theme-item value="popular" selection=selection label="admin.customize.theme.install_popular"}}

View File

@ -5,9 +5,29 @@
<p>
{{i18n "admin.watched_words.test.found_matches"}}
<ul>
{{#each matches as |match|}}
<li>{{match}}</li>
{{/each}}
{{#if (or isReplace isLink)}}
{{#each matches as |match|}}
<li>
<span class="match">{{match.match}}</span>
&rarr;
<span class="replacement">{{match.replacement}}</span>
</li>
{{/each}}
{{else if isTag}}
{{#each matches as |match|}}
<li>
<span class="match">{{match.match}}</span>
&rarr;
{{#each match.tags as |tag|}}
<span class="tag">{{tag}}</span>
{{/each}}
</li>
{{/each}}
{{else}}
{{#each matches as |match|}}
<li>{{match}}</li>
{{/each}}
{{/if}}
</ul>
</p>
{{else}}

View File

@ -1,3 +1,7 @@
{{#if regexpError}}
<div class="alert alert-error">{{regexpError}}</div>
{{/if}}
<div class="watched-word-controls">
{{d-button
class="btn-default download-link"
@ -8,6 +12,7 @@
{{watched-word-uploader uploading=uploading actionKey=actionNameKey done=(action "uploadComplete")}}
{{d-button
class="watched-word-test"
label="admin.watched_words.test.button_label"
icon="far-eye"
action=(action "test")}}
@ -24,20 +29,20 @@
{{watched-word-form
actionKey=actionNameKey
action=(action "recordAdded")
filteredContent=filteredContent
regularExpressions=adminWatchedWords.regularExpressions}}
filteredContent=currentAction.words
}}
{{#if wordCount}}
{{#if currentAction.words}}
<label class="show-words-checkbox">
{{input type="checkbox" checked=adminWatchedWords.showWords disabled=adminWatchedWords.disableShowWords}}
{{i18n "admin.watched_words.show_words" count=wordCount}}
{{i18n "admin.watched_words.show_words" count=currentAction.words.length}}
</label>
{{/if}}
{{#if showWordsList}}
<div class="watched-words-list">
{{#each filteredContent as |word| }}
<div class="watched-word-box">{{admin-watched-word word=word action=(action "recordRemoved")}}</div>
<div class="watched-words-list watched-words-{{actionNameKey}}">
{{#each currentAction.words as |word| }}
<div class="watched-word-box">{{admin-watched-word actionKey=actionNameKey word=word action=(action "recordRemoved")}}</div>
{{/each}}
</div>
{{/if}}

View File

@ -12,7 +12,7 @@
<li class={{action.nameKey}}>
{{#link-to "adminWatchedWords.action" action.nameKey}}
{{action.name}}
{{#if action.count}}<span class="count">({{action.count}})</span>{{/if}}
{{#if action.words}}<span class="count">({{action.words.length}})</span>{{/if}}
{{/link-to}}
</li>
{{/each}}

View File

@ -40,6 +40,7 @@
"ember-source": "~3.15.0",
"ember-source-channel-url": "^2.0.1",
"ember-try": "^1.4.0",
"eslint": "^7.27.0",
"eslint-plugin-ember": "^7.7.1",
"eslint-plugin-node": "^10.0.0",
"loader.js": "^4.7.0"

View File

@ -22,6 +22,7 @@
//= require ./discourse/app/lib/offset-calculator
//= require ./discourse/app/lib/lock-on
//= require ./discourse/app/lib/url
//= require ./discourse/app/lib/email-provider-default-settings
//= require ./discourse/app/lib/debounce
//= require ./discourse/app/lib/quote
//= require ./discourse/app/lib/key-value-store

View File

@ -26,10 +26,6 @@ define("ember-buffered-proxy/proxy", ["exports"], function (__exports__) {
__exports__.default = window.BufferedProxy;
});
define("ember-buffered-proxy/mixin", ["exports"], function (__exports__) {
__exports__.default = null;
});
define("bootbox", ["exports"], function (__exports__) {
__exports__.default = window.bootbox;
});

View File

@ -31,4 +31,9 @@ export default Component.extend({
}
return sanitize(description);
},
@discourseComputed("badge.id")
showFavorite(badgeId) {
return ![1, 2, 3, 4].includes(badgeId);
},
});

View File

@ -308,7 +308,7 @@ export default Component.extend({
customOptions.push({
icon: "globe-americas",
id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE,
label: "bookmarks.reminders.post_local_date",
label: "time_shortcut.post_local_date",
time: this._postLocalDate(),
timeFormatted: this._postLocalDate().format(
I18n.t("dates.long_no_year")

View File

@ -7,6 +7,8 @@ import { filter } from "@ember/object/computed";
export default Component.extend({
classNameBindings: ["hidden:hidden", ":category-breadcrumb"],
tagName: "ol",
editingCategory: false,
editingCategoryTab: null,
@discourseComputed("categories")
filteredCategories(categories) {
@ -47,6 +49,11 @@ export default Component.extend({
});
},
@discourseComputed("siteSettings.tagging_enabled", "editingCategory")
showTagsSection(taggingEnabled, editingCategory) {
return taggingEnabled && !editingCategory;
},
@discourseComputed("category")
parentCategory(category) {
deprecated(

View File

@ -1,5 +1,6 @@
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({
@ -17,6 +18,8 @@ export default Component.extend({
});
},
canDoBulkActions: reads("currentUser.staff"),
actions: {
showBulkActions() {
const controller = showModal("topic-bulk-actions", {

View File

@ -1,6 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "h3",
// icon name defined here so it can be easily overriden in theme components
// icon name defined here so it can be easily overridden in theme components
lockIcon: "lock",
});

View File

@ -269,7 +269,11 @@ export default Component.extend({
if (tl === 0 || tl === 1) {
reason +=
"<br/>" +
I18n.t("composer.error.try_like", { heart: iconHTML("heart") });
I18n.t("composer.error.try_like", {
heart: iconHTML("heart", {
label: I18n.t("likes_lowercase", { count: 1 }),
}),
});
}
}
@ -288,7 +292,7 @@ export default Component.extend({
// when adding two separate files with the same filename search for matching
// placeholder already existing in the editor ie [Uploading: test.png...]
// and add order nr to the next one: [Uplodading: test.png(1)...]
// and add order nr to the next one: [Uploading: test.png(1)...]
const escapedFilename = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regexString = `\\[${I18n.t("uploading_filename", {
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",

View File

@ -129,7 +129,7 @@ class Toolbar {
this.addButton({
id: "code",
group: "insertions",
shortcut: "Shift+C",
shortcut: "E",
preventFocus: true,
trimLeading: true,
action: (...args) => this.context.send("formatCode", args),

View File

@ -4,6 +4,7 @@ export default Component.extend({
classNames: ["modal-body"],
fixed: false,
dismissable: true,
autoFocus: true,
didInsertElement() {
this._super(...arguments);
@ -28,14 +29,6 @@ export default Component.extend({
},
_afterFirstRender() {
if (
!this.site.mobileView &&
this.autoFocus !== "false" &&
this.element.querySelector("input")
) {
this.element.querySelector("input").focus();
}
const maxHeight = this.maxHeight;
if (maxHeight) {
const maxHeightFloat = parseFloat(maxHeight) / 100.0;
@ -57,7 +50,8 @@ export default Component.extend({
"subtitle",
"rawSubtitle",
"dismissable",
"headerClass"
"headerClass",
"autoFocus"
)
);
},

View File

@ -132,13 +132,22 @@ export default Component.extend({
this.set("headerClass", data.headerClass || null);
if (this.element) {
const autofocusInputs = this.element.querySelectorAll(
if (this.element && data.autoFocus) {
let focusTarget = this.element.querySelector(
".modal-body input[autofocus]"
);
if (autofocusInputs.length) {
afterTransition(() => autofocusInputs[0].focus());
if (!focusTarget && !this.site.mobileView) {
focusTarget = this.element.querySelector(
".modal-body input, .modal-body button, .modal-footer input, .modal-footer button"
);
if (!focusTarget) {
focusTarget = this.element.querySelector(".modal-header button");
}
}
if (focusTarget) {
afterTransition(() => focusTarget.focus());
}
}
},

View File

@ -5,4 +5,5 @@ export default Component.extend({
tagName: "tr",
classNameBindings: ["me"],
me: propertyEqual("item.user.id", "currentUser.id"),
columns: null,
});

View File

@ -0,0 +1,17 @@
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
classNames: ["directory-table-container"],
@action
setActiveHeader(header) {
// After render, scroll table left to ensure the order by column is visible
const scrollPixels =
header.offsetLeft + header.offsetWidth + 10 - this.element.offsetWidth;
if (scrollPixels > 0) {
this.element.scrollLeft = scrollPixels;
}
},
});

View File

@ -21,8 +21,17 @@ const DiscoveryTopicsListComponent = Component.extend(UrlRefresh, LoadMore, {
}
},
@observes("topicTrackingState.states")
_updateTopics() {
@on("didInsertElement")
_monitorTrackingState() {
this.topicTrackingState.onStateChange(() => this._updateTrackingTopics());
},
@on("willDestroyElement")
_removeTrackingStateChangeMonitor() {
this.topicTrackingState.offStateChange(this.stateChangeCallbackId);
},
_updateTrackingTopics() {
this.topicTrackingState.updateTopics(this.model.topics);
},

View File

@ -38,12 +38,13 @@ const FooterNavComponent = MountWidget.extend(
}
if (this.capabilities.isIpadOS) {
$("body").addClass("footer-nav-ipad");
document.body.classList.add("footer-nav-ipad");
} else {
this.bindScrolling({ name: "footer-nav" });
$(window).on("resize.footer-nav-on-scroll", () => this.scrolled());
window.addEventListener("resize", this.scrolled, false);
this.appEvents.on("composer:opened", this, "_composerOpened");
this.appEvents.on("composer:closed", this, "_composerClosed");
document.body.classList.add("footer-nav-visible");
}
},
@ -57,10 +58,10 @@ const FooterNavComponent = MountWidget.extend(
}
if (this.capabilities.isIpadOS) {
$("body").removeClass("footer-nav-ipad");
document.body.classList.remove("footer-nav-ipad");
} else {
this.unbindScrolling("footer-nav");
$(window).unbind("resize.footer-nav-on-scroll");
window.removeEventListener("resize", this.scrolled);
this.appEvents.off("composer:opened", this, "_composerOpened");
this.appEvents.off("composer:closed", this, "_composerClosed");
}
@ -77,12 +78,10 @@ const FooterNavComponent = MountWidget.extend(
return;
}
const offset = window.pageYOffset || $("html").scrollTop();
throttle(
this,
this.calculateDirection,
offset,
window.pageYOffset,
MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE
);
},
@ -91,12 +90,11 @@ const FooterNavComponent = MountWidget.extend(
// in the header, otherwise, we hide it.
@observes("mobileScrollDirection")
toggleMobileFooter() {
$(this.element).toggleClass(
this.element.classList.toggle(
"visible",
this.mobileScrollDirection === null ? true : false
);
// body class used to adjust positioning of #topic-progress-wrapper
$("body").toggleClass(
document.body.classList.toggle(
"footer-nav-visible",
this.mobileScrollDirection === null ? true : false
);
@ -126,14 +124,23 @@ const FooterNavComponent = MountWidget.extend(
},
_modalOn() {
postRNWebviewMessage(
"headerBg",
$(".modal-backdrop").css("background-color")
);
const backdrop = document.querySelector(".modal-backdrop");
if (backdrop) {
postRNWebviewMessage(
"headerBg",
getComputedStyle(backdrop)["background-color"]
);
}
},
_modalOff() {
postRNWebviewMessage("headerBg", $(".d-header").css("background-color"));
const dheader = document.querySelector(".d-header");
if (dheader) {
postRNWebviewMessage(
"headerBg",
getComputedStyle(dheader)["background-color"]
);
}
},
goBack() {

View File

@ -24,7 +24,6 @@ export default Component.extend({
date: datetime.format("YYYY-MM-DD"),
time: datetime.format("HH:mm"),
});
this._updateInput();
}
},

View File

@ -0,0 +1,91 @@
import Component from "@ember/component";
import emailProviderDefaultSettings from "discourse/lib/email-provider-default-settings";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import EmberObject, { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
export default Component.extend({
tagName: "",
form: null,
@discourseComputed(
"group.email_username",
"group.email_password",
"form.imap_server",
"form.imap_port"
)
missingSettings(email_username, email_password, imap_server, imap_port) {
return [
email_username,
email_password,
imap_server,
imap_port,
].some((value) => isEmpty(value));
},
@discourseComputed("group.imap_mailboxes")
mailboxes(imapMailboxes) {
if (!imapMailboxes) {
return [];
}
return imapMailboxes.map((mailbox) => ({ name: mailbox, value: mailbox }));
},
@discourseComputed("group.imap_mailbox_name", "mailboxes.length")
mailboxSelected(mailboxName, mailboxesSize) {
return mailboxesSize === 0 || !isEmpty(mailboxName);
},
@action
resetSettingsValid() {
this.set("imapSettingsValid", false);
},
@on("init")
_fillForm() {
this.set(
"form",
EmberObject.create({
imap_server: this.group.imap_server,
imap_port: (this.group.imap_port || "").toString(),
imap_ssl: this.group.imap_ssl,
})
);
},
@action
prefillSettings(provider) {
this.form.setProperties(emailProviderDefaultSettings(provider, "imap"));
},
@action
testImapSettings() {
const settings = {
host: this.form.imap_server,
port: this.form.imap_port,
ssl: this.form.imap_ssl,
username: this.group.email_username,
password: this.group.email_password,
};
this.set("testingSettings", true);
this.set("imapSettingsValid", false);
return ajax(`/groups/${this.group.id}/test_email_settings`, {
type: "POST",
data: Object.assign(settings, { protocol: "imap" }),
})
.then(() => {
this.set("imapSettingsValid", true);
this.group.setProperties({
imap_server: this.form.imap_server,
imap_port: this.form.imap_port,
imap_ssl: this.form.imap_ssl,
});
})
.catch(popupAjaxError)
.finally(() => this.set("testingSettings", false));
},
});

View File

@ -0,0 +1,116 @@
import Component from "@ember/component";
import { isEmpty } from "@ember/utils";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import I18n from "I18n";
import bootbox from "bootbox";
import { action } from "@ember/object";
export default Component.extend({
tagName: "",
imapSettingsValid: false,
smtpSettingsValid: false,
@on("init")
_determineSettingsValid() {
this.set(
"imapSettingsValid",
this.group.imap_enabled && this.group.imap_server
);
this.set(
"smtpSettingsValid",
this.group.smtp_enabled && this.group.smtp_server
);
},
@discourseComputed(
"emailSettingsValid",
"group.smtp_enabled",
"group.imap_enabled"
)
enableImapSettings(emailSettingsValid, smtpEnabled, imapEnabled) {
return smtpEnabled && (emailSettingsValid || imapEnabled);
},
@discourseComputed(
"smtpSettingsValid",
"imapSettingsValid",
"group.smtp_enabled",
"group.imap_enabled"
)
emailSettingsValid(
smtpSettingsValid,
imapSettingsValid,
smtpEnabled,
imapEnabled
) {
return (
(!smtpEnabled || smtpSettingsValid) && (!imapEnabled || imapSettingsValid)
);
},
_anySmtpFieldsFilled() {
return [
this.group.smtp_server,
this.group.smtp_port,
this.group.email_username,
this.group.email_password,
].some((value) => !isEmpty(value));
},
_anyImapFieldsFilled() {
return [this.group.imap_server, this.group.imap_port].some(
(value) => !isEmpty(value)
);
},
@action
smtpEnabledChange(event) {
if (
!event.target.checked &&
this.group.smtp_enabled &&
this._anySmtpFieldsFilled()
) {
bootbox.confirm(
I18n.t("groups.manage.email.smtp_disable_confirm"),
(result) => {
if (!result) {
this.group.set("smtp_enabled", true);
} else {
this.group.set("imap_enabled", false);
}
}
);
}
this.group.set("smtp_enabled", event.target.checked);
},
@action
imapEnabledChange(event) {
if (
!event.target.checked &&
this.group.imap_enabled &&
this._anyImapFieldsFilled()
) {
bootbox.confirm(
I18n.t("groups.manage.email.imap_disable_confirm"),
(result) => {
if (!result) {
this.group.set("imap_enabled", true);
}
}
);
}
this.group.set("imap_enabled", event.target.checked);
},
@action
afterSave() {
// reload the group to get the updated imap_mailboxes
this.store.find("group", this.group.name).then(() => {
this._determineSettingsValid();
});
},
});

View File

@ -7,6 +7,7 @@ import { popupAutomaticMembershipAlert } from "discourse/controllers/groups-new"
export default Component.extend({
saving: null,
disabled: false,
@discourseComputed("saving")
savingText(saving) {
@ -15,6 +16,10 @@ export default Component.extend({
actions: {
save() {
if (this.beforeSave) {
this.beforeSave();
}
this.set("saving", true);
const group = this.model;
@ -31,6 +36,10 @@ export default Component.extend({
}
this.set("saved", true);
if (this.afterSave) {
this.afterSave();
}
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));

View File

@ -0,0 +1,82 @@
import Component from "@ember/component";
import emailProviderDefaultSettings from "discourse/lib/email-provider-default-settings";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import EmberObject, { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
export default Component.extend({
tagName: "",
form: null,
@discourseComputed(
"form.email_username",
"form.email_password",
"form.smtp_server",
"form.smtp_port"
)
missingSettings(email_username, email_password, smtp_server, smtp_port) {
return [
email_username,
email_password,
smtp_server,
smtp_port,
].some((value) => isEmpty(value));
},
@action
resetSettingsValid() {
this.set("smtpSettingsValid", false);
},
@on("init")
_fillForm() {
this.set(
"form",
EmberObject.create({
email_username: this.group.email_username,
email_password: this.group.email_password,
smtp_server: this.group.smtp_server,
smtp_port: (this.group.smtp_port || "").toString(),
smtp_ssl: this.group.smtp_ssl,
})
);
},
@action
prefillSettings(provider) {
this.form.setProperties(emailProviderDefaultSettings(provider, "smtp"));
},
@action
testSmtpSettings() {
const settings = {
host: this.form.smtp_server,
port: this.form.smtp_port,
ssl: this.form.smtp_ssl,
username: this.form.email_username,
password: this.form.email_password,
};
this.set("testingSettings", true);
this.set("smtpSettingsValid", false);
return ajax(`/groups/${this.group.id}/test_email_settings`, {
type: "POST",
data: Object.assign(settings, { protocol: "smtp" }),
})
.then(() => {
this.set("smtpSettingsValid", true);
this.group.setProperties({
smtp_server: this.form.smtp_server,
smtp_port: this.form.smtp_port,
smtp_ssl: this.form.smtp_ssl,
email_username: this.form.email_username,
email_password: this.form.email_password,
});
})
.catch(popupAjaxError)
.finally(() => this.set("testingSettings", false));
},
});

View File

@ -52,6 +52,10 @@ export default Component.extend(FilterModeMixin, {
},
ensureDropClosed() {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
if (this.expanded) {
this.set("expanded", false);
}
@ -75,17 +79,13 @@ export default Component.extend(FilterModeMixin, {
this.element.querySelector(".drop").style.display = "none";
next(() => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
this.set("expanded", false);
this.ensureDropClosed();
});
return true;
});
$(window).on("click.navigation-bar", () => {
this.set("expanded", false);
this.ensureDropClosed();
return true;
});
});

View File

@ -5,6 +5,7 @@ import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
classNameBindings: [":popup-tip", "good", "bad", "lastShownAt::hide"],
attributeBindings: ["role"],
animateAttribute: null,
bouncePixels: 6,
bounceDelay: 100,
@ -12,6 +13,13 @@ export default Component.extend({
closeIcon: `${iconHTML("times-circle")}`.htmlSafe(),
tipReason: null,
@discourseComputed("bad")
role(bad) {
if (bad) {
return "alert";
}
},
click() {
this.set("shownAt", null);
this.set("validation.lastShownAt", null);

View File

@ -29,12 +29,17 @@ export default Component.extend({
@discourseComputed(
"reviewable.type",
"reviewable.stale",
"siteSettings.blur_tl0_flagged_posts_media",
"reviewable.target_created_by_trust_level"
)
customClasses(type, blurEnabled, trustLevel) {
customClasses(type, stale, blurEnabled, trustLevel) {
let classes = type.dasherize();
if (stale) {
classes = `${classes} reviewable-stale`;
}
if (blurEnabled && trustLevel === 0) {
classes = `${classes} blur-images`;
}

View File

@ -1,6 +1,5 @@
import PanEvents, {
SWIPE_DISTANCE_THRESHOLD,
SWIPE_VELOCITY,
SWIPE_VELOCITY_THRESHOLD,
} from "discourse/mixins/pan-events";
import { cancel, later, schedule } from "@ember/runloop";
@ -23,7 +22,6 @@ const SiteHeaderComponent = MountWidget.extend(
_isPanning: false,
_panMenuOrigin: "right",
_panMenuOffset: 0,
_scheduledMovingAnimation: null,
_scheduledRemoveAnimate: null,
_topic: null,
_mousetrap: null,
@ -37,26 +35,44 @@ const SiteHeaderComponent = MountWidget.extend(
this.queueRerender();
},
_animateOpening($panel) {
$panel.css({ right: "", left: "" });
_animateOpening(panel) {
const headerCloak = document.querySelector(".header-cloak");
panel.classList.add("animate");
headerCloak.classList.add("animate");
this._scheduledRemoveAnimate = later(() => {
panel.classList.remove("animate");
headerCloak.classList.remove("animate");
}, 200);
panel.style.setProperty("--offset", 0);
headerCloak.style.setProperty("--opacity", 0.5);
this._panMenuOffset = 0;
},
_animateClosing($panel, menuOrigin, windowWidth) {
$panel.css(menuOrigin, -windowWidth);
_animateClosing(panel, menuOrigin) {
const windowWidth = document.body.offsetWidth;
this._animate = true;
schedule("afterRender", () => {
this.eventDispatched("dom:clean", "header");
this._panMenuOffset = 0;
});
const headerCloak = document.querySelector(".header-cloak");
panel.classList.add("animate");
headerCloak.classList.add("animate");
const offsetDirection = menuOrigin === "left" ? -1 : 1;
panel.style.setProperty("--offset", `${offsetDirection * windowWidth}px`);
headerCloak.style.setProperty("--opacity", 0);
this._scheduledRemoveAnimate = later(() => {
panel.classList.remove("animate");
headerCloak.classList.remove("animate");
schedule("afterRender", () => {
this.eventDispatched("dom:clean", "header");
this._panMenuOffset = 0;
});
}, 200);
},
_isRTL() {
return $("html").css("direction") === "rtl";
return document.querySelector("html").classList["direction"] === "rtl";
},
_leftMenuClass() {
return this._isRTL() ? ".user-menu" : ".hamburger-panel";
return this._isRTL() ? "user-menu" : "hamburger-panel";
},
_leftMenuAction() {
@ -67,28 +83,14 @@ const SiteHeaderComponent = MountWidget.extend(
return this._isRTL() ? "toggleHamburger" : "toggleUserMenu";
},
_handlePanDone(offset, event) {
const $window = $(window);
const windowWidth = $window.width();
const $menuPanels = $(".menu-panel");
_handlePanDone(event) {
const menuPanels = document.querySelectorAll(".menu-panel");
const menuOrigin = this._panMenuOrigin;
this._shouldMenuClose(event, menuOrigin)
? (offset += SWIPE_VELOCITY)
: (offset -= SWIPE_VELOCITY);
$menuPanels.each((idx, panel) => {
const $panel = $(panel);
const $headerCloak = $(".header-cloak");
$panel.css(menuOrigin, -offset);
$headerCloak.css("opacity", Math.min(0.5, (300 - offset) / 600));
if (offset > windowWidth) {
this._animateClosing($panel, menuOrigin, windowWidth);
} else if (offset <= 0) {
this._animateOpening($panel);
menuPanels.forEach((panel) => {
if (this._shouldMenuClose(event, menuOrigin)) {
this._animateClosing(panel, menuOrigin);
} else {
//continue to open or close menu
this._scheduledMovingAnimation = window.requestAnimationFrame(() =>
this._handlePanDone(offset, event)
);
this._animateOpening(panel);
}
});
},
@ -114,11 +116,15 @@ const SiteHeaderComponent = MountWidget.extend(
panStart(e) {
const center = e.center;
const $centeredElement = $(document.elementFromPoint(center.x, center.y));
const panOverValidElement = document
.elementsFromPoint(center.x, center.y)
.some(
(ele) =>
ele.classList.contains("panel-body") ||
ele.classList.contains("header-cloak")
);
if (
($centeredElement.hasClass("panel-body") ||
$centeredElement.hasClass("header-cloak") ||
$centeredElement.parents(".panel-body").length) &&
panOverValidElement &&
(e.direction === "left" || e.direction === "right")
) {
e.originalEvent.preventDefault();
@ -133,57 +139,51 @@ const SiteHeaderComponent = MountWidget.extend(
return;
}
this._isPanning = false;
$(".menu-panel").each((idx, panel) => {
const $panel = $(panel);
let offset = $panel.css("right");
if (this._panMenuOrigin === "left") {
offset = $panel.css("left");
}
offset = Math.abs(parseInt(offset, 10));
this._handlePanDone(offset, e);
});
this._handlePanDone(e);
},
panMove(e) {
if (!this._isPanning) {
return;
}
const $menuPanels = $(".menu-panel");
$menuPanels.each((idx, panel) => {
const $panel = $(panel);
const $headerCloak = $(".header-cloak");
if (this._panMenuOrigin === "right") {
const pxClosed = Math.min(0, -e.deltaX + this._panMenuOffset);
$panel.css("right", pxClosed);
$headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600));
} else {
const pxClosed = Math.min(0, e.deltaX + this._panMenuOffset);
$panel.css("left", pxClosed);
$headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600));
}
});
const panel = document.querySelector(".menu-panel");
const headerCloak = document.querySelector(".header-cloak");
if (this._panMenuOrigin === "right") {
const pxClosed = Math.min(0, -e.deltaX + this._panMenuOffset);
panel.style.setProperty("--offset", `${-pxClosed}px`);
headerCloak.style.setProperty(
"--opacity",
Math.min(0.5, (300 + pxClosed) / 600)
);
} else {
const pxClosed = Math.min(0, e.deltaX + this._panMenuOffset);
panel.style.setProperty("--offset", `${pxClosed}px`);
headerCloak.style.setProperty(
"--opacity",
Math.min(0.5, (300 + pxClosed) / 600)
);
}
},
dockCheck(info) {
const $header = $("header.d-header");
const header = document.querySelector("header.d-header");
if (this.docAt === null) {
if (!($header && $header.length === 1)) {
if (!header) {
return;
}
this.docAt = $header.offset().top;
this.docAt = header.offsetTop;
}
const $body = $("body");
const offset = info.offset();
if (offset >= this.docAt) {
if (!this.dockedHeader) {
$body.addClass("docked");
document.body.classList.add("docked");
this.dockedHeader = true;
}
} else {
if (this.dockedHeader) {
$body.removeClass("docked");
document.body.classList.remove("docked");
this.dockedHeader = false;
}
}
@ -197,13 +197,14 @@ const SiteHeaderComponent = MountWidget.extend(
willRender() {
if (this.get("currentUser.staff")) {
$("body").addClass("staff");
document.body.classList.add("staff");
}
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
this._resizeDiscourseMenuPanel = () => this.afterRender();
window.addEventListener("resize", this._resizeDiscourseMenuPanel);
this.appEvents.on("header:show-topic", this, "setTopic");
this.appEvents.on("header:hide-topic", this, "setTopic");
@ -279,14 +280,13 @@ const SiteHeaderComponent = MountWidget.extend(
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.discourse-menu-panel");
window.removeEventListener("resize", this._resizeDiscourseMenuPanel);
this.appEvents.off("header:show-topic", this, "setTopic");
this.appEvents.off("header:hide-topic", this, "setTopic");
this.appEvents.off("dom:clean", this, "_cleanDom");
cancel(this._scheduledRemoveAnimate);
window.cancelAnimationFrame(this._scheduledMovingAnimation);
this._mousetrap.reset();
@ -308,25 +308,24 @@ const SiteHeaderComponent = MountWidget.extend(
);
}
const $menuPanels = $(".menu-panel");
if ($menuPanels.length === 0) {
const menuPanels = document.querySelectorAll(".menu-panel");
if (menuPanels.length === 0) {
if (this.site.mobileView) {
this._animate = true;
}
return;
}
const $window = $(window);
const windowWidth = $window.width();
const headerWidth = $("#main-outlet .container").width() || 1100;
const windowWidth = document.body.offsetWidth;
const headerWidth =
document.querySelector("#main-outlet .container").offsetWidth || 1100;
const remaining = (windowWidth - headerWidth) / 2;
const viewMode = remaining < 50 ? "slide-in" : "drop-down";
const viewMode =
this.site.mobileView || remaining < 50 ? "slide-in" : "drop-down";
$menuPanels.each((idx, panel) => {
const $panel = $(panel);
const $headerCloak = $(".header-cloak");
let width = parseInt($panel.attr("data-max-width"), 10) || 300;
menuPanels.forEach((panel) => {
const headerCloak = document.querySelector(".header-cloak");
let width = parseInt(panel.getAttribute("data-max-width"), 10) || 300;
if (windowWidth - width < 50) {
width = windowWidth - 50;
}
@ -334,51 +333,51 @@ const SiteHeaderComponent = MountWidget.extend(
this._panMenuOffset = -width;
}
$panel.removeClass("drop-down slide-in").addClass(viewMode);
panel.classList.remove("drop-down");
panel.classList.remove("slide-in");
panel.classList.add(viewMode);
if (this._animate || this._panMenuOffset !== 0) {
$headerCloak.css("opacity", 0);
if (
this.site.mobileView &&
$panel.parent(this._leftMenuClass()).length > 0
panel.parentElement.classList.contains(this._leftMenuClass())
) {
this._panMenuOrigin = "left";
$panel.css("left", -windowWidth);
panel.style.setProperty("--offset", `${-windowWidth}px`);
} else {
this._panMenuOrigin = "right";
$panel.css("right", -windowWidth);
panel.style.setProperty("--offset", `${windowWidth}px`);
}
headerCloak.style.setProperty("--opacity", 0);
}
const $panelBody = $(".panel-body", $panel);
const panelBody = panel.querySelector(".panel-body");
// We use a mutationObserver to check for style changes, so it's important
// we don't set it if it doesn't change. Same goes for the $panelBody!
const style = $panel.prop("style");
// we don't set it if it doesn't change. Same goes for the panelBody!
if (viewMode === "drop-down") {
const $buttonPanel = $("header ul.icons");
if ($buttonPanel.length === 0) {
const buttonPanel = document.querySelectorAll("header ul.icons");
if (buttonPanel.length === 0) {
return;
}
// These values need to be set here, not in the css file - this is to deal with the
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
if (style.top !== "100%" || style.height !== "auto") {
$panel.css({ top: "100%", height: "auto" });
if (panel.style.top !== "100%" || panel.style.height !== "auto") {
panel.style.setProperty("top", "100%");
panel.style.setProperty("height", "auto");
}
$("body").addClass("drop-down-mode");
document.body.classList.add("drop-down-mode");
} else {
if (this.site.mobileView) {
$headerCloak.show();
headerCloak.style.display = "block";
}
const menuTop = this.site.mobileView ? headerTop() : headerHeight();
const winHeightOffset = 16;
let initialWinHeight = window.innerHeight
? window.innerHeight
: $(window).height();
let initialWinHeight = window.innerHeight;
const winHeight = initialWinHeight - winHeightOffset;
let height;
@ -394,27 +393,26 @@ const SiteHeaderComponent = MountWidget.extend(
height = winHeight - menuTop - iPadOffset;
}
if ($panelBody.prop("style").height !== "100%") {
$panelBody.height("100%");
if (panelBody.style.height !== "100%") {
panelBody.style.setProperty("height", "100%");
}
if (style.top !== menuTop + "px" || style[heightProp] !== height) {
$panel.css({ top: menuTop + "px", [heightProp]: height });
$(".header-cloak").css({ top: menuTop + "px" });
if (
panel.style.top !== `${menuTop}px` ||
panel.style[heightProp] !== `${height}px`
) {
panel.style.top = `${menuTop}px`;
panel.style.setProperty(heightProp, `${height}px`);
if (headerCloak) {
headerCloak.style.top = `${menuTop}px`;
}
}
$("body").removeClass("drop-down-mode");
document.body.classList.remove("drop-down-mode");
}
$panel.width(width);
panel.style.setProperty("width", `${width}px`);
if (this._animate) {
$panel.addClass("animate");
$headerCloak.addClass("animate");
this._scheduledRemoveAnimate = later(() => {
$panel.removeClass("animate");
$headerCloak.removeClass("animate");
}, 200);
this._animateOpening(panel);
}
$panel.css({ right: "", left: "" });
$headerCloak.css("opacity", 0.5);
this._animate = false;
});
},
@ -426,21 +424,19 @@ export default SiteHeaderComponent.extend({
});
export function headerHeight() {
const $header = $("header.d-header");
const header = document.querySelector("header.d-header");
// Header may not exist in tests (e.g. in the user menu component test).
if ($header.length === 0) {
if (!header) {
return 0;
}
const headerOffset = $header.offset();
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
return $header.outerHeight() + headerOffsetTop - $(window).scrollTop();
const headerOffsetTop = header.offsetTop ? header.offsetTop : 0;
return header.offsetHeight + headerOffsetTop - document.body.scrollTop;
}
export function headerTop() {
const $header = $("header.d-header");
const headerOffset = $header.offset();
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
return headerOffsetTop - $(window).scrollTop();
const header = document.querySelector("header.d-header");
const headerOffsetTop = header.offsetTop ? header.offsetTop : 0;
return headerOffsetTop - document.body.scrollTop;
}

View File

@ -9,6 +9,7 @@ export default Component.extend({
tagName: "",
showPrompt: false,
animatePrompt: false,
_timeoutHandler: null,
@discourseComputed
@ -29,18 +30,34 @@ export default Component.extend({
if (!this._timeoutHandler && this.session.requiresRefresh) {
if (isTesting()) {
this.set("showPrompt", true);
this.updatePromptState(true);
} else {
// Since we can do this transparently for people browsing the forum
// hold back the message 24 hours.
this._timeoutHandler = later(() => {
this.set("showPrompt", true);
this.updatePromptState(true);
}, 1000 * 60 * 24 * 60);
}
}
});
},
updatePromptState(value) {
// when adding the message, we inject the HTML then add the animation
// when dismissing, things need to happen in the opposite order
const firstProp = value ? "showPrompt" : "animatePrompt",
secondProp = value ? "animatePrompt" : "showPrompt";
this.set(firstProp, value);
if (isTesting()) {
this.set(secondProp, value);
} else {
later(() => {
this.set(secondProp, value);
}, 500);
}
},
@action
refreshPage() {
document.location.reload();
@ -48,7 +65,7 @@ export default Component.extend({
@action
dismiss() {
this.set("showPrompt", false);
this.updatePromptState(false);
},
@on("willDestroyElement")

View File

@ -1,6 +1,4 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
@ -10,15 +8,8 @@ export default Component.extend({
labelKey: null,
chevronIcon: null,
columnIcon: null,
@discourseComputed("field", "labelKey")
title(field, labelKey) {
if (!labelKey) {
labelKey = `directory.${this.field}`;
}
return I18n.t(labelKey + "_long", { defaultValue: I18n.t(labelKey) });
},
translated: false,
onActiveRender: null,
toggleProperties() {
if (this.order === this.field) {
@ -40,13 +31,12 @@ export default Component.extend({
},
didReceiveAttrs() {
this._super(...arguments);
this.set("id", `table-header-toggle-${this.field.replace(/\s/g, "")}`);
this.toggleChevron();
},
init() {
this._super(...arguments);
if (this.icon) {
let columnIcon = iconHTML(this.icon);
this.set("columnIcon", `${columnIcon}`.htmlSafe());
didRender() {
if (this.onActiveRender && this.chevronIcon) {
this.onActiveRender(this.element);
}
},
});

View File

@ -5,9 +5,11 @@ import PermissionType from "discourse/models/permission-type";
import bootbox from "bootbox";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
import { isEmpty } from "@ember/utils";
export default Component.extend(bufferedProperty("model"), {
router: service(),
tagName: "",
allGroups: null,
@ -36,15 +38,6 @@ export default Component.extend(bufferedProperty("model"), {
);
},
@discourseComputed("buffered.permissions")
showPrivateChooser(permissions) {
if (!permissions) {
return true;
}
return permissions.everyone !== PermissionType.READONLY;
},
@discourseComputed("buffered.permissions", "allGroups")
selectedGroupIds(permissions, allGroups) {
if (!permissions || !allGroups) {
@ -140,6 +133,8 @@ export default Component.extend(bufferedProperty("model"), {
if (this.onSave) {
this.onSave();
} else {
this.router.transitionTo("tagGroups.index");
}
});
},

View File

@ -8,7 +8,7 @@ export default DropdownSelectBoxComponent.extend({
actionsMapping: null,
selectKitOptions: {
icons: ["bars", "caret-down"],
icons: ["wrench", "caret-down"],
showFullTitle: false,
},
@ -18,7 +18,7 @@ export default DropdownSelectBoxComponent.extend({
id: "manageGroups",
name: I18n.t("tagging.manage_groups"),
description: I18n.t("tagging.manage_groups_description"),
icon: "wrench",
icon: "tags",
},
{
id: "uploadTags",

View File

@ -0,0 +1,105 @@
import { action } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { later } from "@ember/runloop";
import isElementInViewport from "discourse/lib/is-element-in-viewport";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import I18n from "I18n";
import Component from "@ember/component";
export default Component.extend({
tagName: "",
classNames: ["topic-dismiss-buttons"],
position: null,
selectedTopics: null,
model: null,
@discourseComputed("position")
containerClass(position) {
return `dismiss-container-${position}`;
},
@discourseComputed("position")
dismissReadId(position) {
return `dismiss-topics-${position}`;
},
@discourseComputed("position")
dismissNewId(position) {
return `dismiss-new-${position}`;
},
@discourseComputed(
"position",
"isOtherDismissUnreadButtonVisible",
"isOtherDismissNewButtonVisible"
)
showBasedOnPosition(
position,
isOtherDismissUnreadButtonVisible,
isOtherDismissNewButtonVisible
) {
if (position !== "top") {
return true;
}
return !(
isOtherDismissUnreadButtonVisible || isOtherDismissNewButtonVisible
);
},
@discourseComputed("selectedTopics.length")
dismissLabel(selectedTopicCount) {
if (selectedTopicCount === 0) {
return I18n.t("topics.bulk.dismiss_button");
}
return I18n.t("topics.bulk.dismiss_button_with_selected", {
count: selectedTopicCount,
});
},
@discourseComputed("selectedTopics.length")
dismissNewLabel(selectedTopicCount) {
if (selectedTopicCount === 0) {
return I18n.t("topics.bulk.dismiss_new");
}
return I18n.t("topics.bulk.dismiss_new_with_selected", {
count: selectedTopicCount,
});
},
// we want to only render the Dismiss... button at the top of the
// page if the user cannot see the bottom Dismiss... button based on their
// viewport, or if too many topics fill the page
@on("didInsertElement")
_determineOtherDismissVisibility() {
later(() => {
if (this.position === "top") {
this.set(
"isOtherDismissUnreadButtonVisible",
isElementInViewport(document.getElementById("dismiss-topics-bottom"))
);
this.set(
"isOtherDismissNewButtonVisible",
isElementInViewport(document.getElementById("dismiss-new-bottom"))
);
} else {
this.set("isOtherDismissUnreadButtonVisible", true);
this.set("isOtherDismissNewButtonVisible", true);
}
});
},
@action
dismissReadPosts() {
let dismissTitle = "topics.bulk.dismiss_read";
if (this.selectedTopics.length > 0) {
dismissTitle = "topics.bulk.dismiss_read_with_selected";
}
showModal("dismiss-read", {
titleTranslated: I18n.t(dismissTitle, {
count: this.selectedTopics.length,
}),
});
},
});

View File

@ -6,6 +6,10 @@ import { getTopicFooterButtons } from "discourse/lib/register-topic-footer-butto
export default Component.extend({
elementId: "topic-footer-buttons",
attributeBindings: ["role"],
role: "region",
// Allow us to extend it
layoutName: "components/topic-footer-buttons",

View File

@ -38,8 +38,10 @@ export function navigateToTopic(topic, href) {
export default Component.extend({
tagName: "tr",
classNameBindings: [":topic-list-item", "unboundClassNames", "topic.visited"],
attributeBindings: ["data-topic-id"],
attributeBindings: ["data-topic-id", "role", "ariaLevel:aria-level"],
"data-topic-id": alias("topic.id"),
role: "heading",
ariaLevel: "2",
didReceiveAttrs() {
this._super(...arguments);

View File

@ -1,6 +1,5 @@
import PanEvents, {
SWIPE_DISTANCE_THRESHOLD,
SWIPE_VELOCITY,
SWIPE_VELOCITY_THRESHOLD,
} from "discourse/mixins/pan-events";
import Component from "@ember/component";
@ -127,17 +126,18 @@ export default Component.extend(PanEvents, {
const $timelineContainer = $(".timeline-container");
const maxOffset = parseInt($timelineContainer.css("height"), 10);
this._shouldPanClose(event)
? (offset += SWIPE_VELOCITY)
: (offset -= SWIPE_VELOCITY);
$timelineContainer.css("bottom", -offset);
if (offset > maxOffset) {
this._collapseFullscreen();
} else if (offset <= 0) {
$timelineContainer.css("bottom", "");
$timelineContainer.addClass("animate");
if (this._shouldPanClose(event)) {
$timelineContainer.css("--offset", `${maxOffset}px`);
later(() => {
this._collapseFullscreen();
$timelineContainer.removeClass("animate");
}, 200);
} else {
later(() => this._handlePanDone(offset, event), 20);
$timelineContainer.css("--offset", 0);
later(() => {
$timelineContainer.removeClass("animate");
}, 200);
}
},
@ -174,7 +174,7 @@ export default Component.extend(PanEvents, {
return;
}
e.originalEvent.preventDefault();
$(".timeline-container").css("bottom", Math.min(0, -e.deltaY));
$(".timeline-container").css("--offset", `${Math.max(0, e.deltaY)}px`);
},
didInsertElement() {

View File

@ -13,7 +13,7 @@ export default Component.extend({
if (path === "faq" || path === "guidelines") {
$(window).on("load.faq resize.faq scroll.faq", () => {
const faqUnread = !currentUser.get("read_faq");
if (faqUnread && isElementInViewport($(".contents p").last())) {
if (faqUnread && isElementInViewport($(".contents p").last()[0])) {
this.action();
}
});

View File

@ -6,20 +6,15 @@ import { gt } from "@ember/object/computed";
export default Controller.extend({
faqOverriden: gt("siteSettings.faq_url.length", 0),
@discourseComputed
contactInfo() {
if (this.siteSettings.contact_url) {
@discourseComputed("model.contact_url", "model.contact_email")
contactInfo(url, email) {
if (url) {
return I18n.t("about.contact_info", {
contact_info:
"<a href='" +
this.siteSettings.contact_url +
"' target='_blank'>" +
this.siteSettings.contact_url +
"</a>",
contact_info: `<a href='${url}' target='_blank'>${url}</a>`,
});
} else if (this.siteSettings.contact_email) {
} else if (email) {
return I18n.t("about.contact_info", {
contact_info: this.siteSettings.contact_email,
contact_info: email,
});
} else {
return null;

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
@ -9,7 +9,7 @@ import { isEmpty } from "@ember/utils";
import { next } from "@ember/runloop";
export default Controller.extend(ModalFunctionality, {
topicController: inject("topic"),
topicController: controller("topic"),
saving: false,
newOwner: null,

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
@ -9,7 +9,7 @@ import { next } from "@ember/runloop";
// Modal related to changing the timestamp of posts
export default Controller.extend(ModalFunctionality, {
topicController: inject("topic"),
topicController: controller("topic"),
saving: false,
date: "",
time: "",

View File

@ -1,5 +1,5 @@
import Composer, { SAVE_ICONS, SAVE_LABELS } from "discourse/models/composer";
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { action, computed } from "@ember/object";
import { alias, and, or, reads } from "@ember/object/computed";
import {
@ -93,7 +93,7 @@ export function addPopupMenuOptionsCallback(callback) {
}
export default Controller.extend({
topicController: inject("topic"),
topicController: controller("topic"),
router: service(),
checkedMessages: false,
@ -101,6 +101,7 @@ export default Controller.extend({
showEditReason: false,
editReason: null,
scopedCategoryId: null,
prioritizedCategoryId: null,
lastValidatedAt: null,
isUploading: false,
topic: null,
@ -710,7 +711,10 @@ export default Controller.extend({
composer.set("disableDrafts", true);
// for now handle a very narrow use case
// if we are replying to a topic AND not on the topic pop the window up
// if we are replying to a topic
// AND are on on a different topic
// AND topic is open (or we are staff)
// --> pop the window up
if (!force && composer.replyingToTopic) {
const currentTopic = this.topicModel;
@ -719,7 +723,10 @@ export default Controller.extend({
return;
}
if (currentTopic.id !== composer.get("topic.id")) {
if (
currentTopic.id !== composer.get("topic.id") &&
(this.isStaffUser || !currentTopic.closed)
) {
const message =
"<h1>" + I18n.t("composer.posting_not_on_topic") + "</h1>";
@ -763,14 +770,15 @@ export default Controller.extend({
// TODO: This should not happen in model
const imageSizes = {};
$("#reply-control .d-editor-preview img").each((i, e) => {
const $img = $(e);
const src = $img.prop("src");
document
.querySelectorAll("#reply-control .d-editor-preview img")
.forEach((e) => {
const src = e.src;
if (src && src.length) {
imageSizes[src] = { width: $img.width(), height: $img.height() };
}
});
if (src && src.length) {
imageSizes[src] = { width: e.naturalWidth, height: e.naturalHeight };
}
});
const promise = composer
.save({ imageSizes, editReason: this.editReason })
@ -868,15 +876,22 @@ export default Controller.extend({
},
/**
Open the composer view
Open the composer view
@method open
@param {Object} opts Options for creating a post
@param {String} opts.action The action we're performing: edit, reply or createTopic
@param {Post} [opts.post] The post we're replying to
@param {Topic} [opts.topic] The topic we're replying to
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
**/
@method open
@param {Object} opts Options for creating a post
@param {String} opts.action The action we're performing: edit, reply, createTopic, createSharedDraft, privateMessage
@param {String} opts.draftKey
@param {Post} [opts.post] The post we're replying to
@param {Topic} [opts.topic] The topic we're replying to
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
@param {Boolean} [opts.ignoreIfChanged]
@param {Boolean} [opts.disableScopedCategory]
@param {Number} [opts.categoryId] Sets `scopedCategoryId` and `categoryId` on the Composer model
@param {Number} [opts.prioritizedCategoryId]
@param {String} [opts.draftSequence]
@param {Boolean} [opts.skipDraftCheck]
**/
open(opts) {
opts = opts || {};
@ -898,6 +913,7 @@ export default Controller.extend({
showEditReason: false,
editReason: null,
scopedCategoryId: null,
prioritizedCategoryId: null,
skipAutoSave: true,
});
@ -909,6 +925,16 @@ export default Controller.extend({
}
}
if (opts.prioritizedCategoryId) {
const category = this.site.categories.findBy(
"id",
opts.prioritizedCategoryId
);
if (category) {
this.set("prioritizedCategoryId", opts.prioritizedCategoryId);
}
}
// If we want a different draft than the current composer, close it and clear our model.
if (
composerModel &&
@ -1137,11 +1163,11 @@ export default Controller.extend({
let promise = new Promise((resolve, reject) => {
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
const controller = showModal("discard-draft", {
const modal = showModal("discard-draft", {
model: this.model,
modalClass: "discard-draft-modal",
});
controller.setProperties({
modal.setProperties({
onDestroyDraft: () => {
this.destroyDraft()
.then(() => {

View File

@ -268,7 +268,7 @@ export default Controller.extend(
(isEmpty(this.accountUsername) || this.get("authOptions.email"))
) {
// If email is valid and username has not been entered yet,
// or email and username were filled automatically by 3rd parth auth,
// or email and username were filled automatically by 3rd party auth,
// then look for a registered username that matches the email.
discourseDebounce(this, this.fetchExistingUsername, 500);
}

View File

@ -1,11 +1,11 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import { empty, notEmpty } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import { getNativeContact } from "discourse/lib/pwa-utils";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { getNativeContact } from "discourse/lib/pwa-utils";
import Group from "discourse/models/group";
import Invite from "discourse/models/invite";
import I18n from "I18n";
@ -24,7 +24,8 @@ export default Controller.extend(
limitToEmail: false,
autogenerated: false,
type: "link",
isLink: empty("buffered.email"),
isEmail: notEmpty("buffered.email"),
onShow() {
Group.findAll().then((groups) => {
@ -52,10 +53,7 @@ export default Controller.extend(
},
setInvite(invite) {
this.setProperties({
invite,
type: invite.email ? "email" : "link",
});
this.set("invite", invite);
},
setAutogenerated(value) {
@ -70,7 +68,7 @@ export default Controller.extend(
const data = { ...this.buffered.buffer };
if (data.groupIds !== undefined) {
data.group_ids = data.groupIds;
data.group_ids = data.groupIds.length > 0 ? data.groupIds : "";
delete data.groupIds;
}
@ -80,13 +78,12 @@ export default Controller.extend(
delete data.topicTitle;
}
if (this.type === "link") {
if (this.buffered.get("email")) {
data.email = "";
data.custom_message = "";
if (this.isLink) {
if (this.invite.email) {
data.email = data.custom_message = "";
}
} else if (this.type === "email") {
if (this.buffered.get("max_redemptions_allowed") > 1) {
} else if (this.isEmail) {
if (this.invite.max_redemptions_allowed > 1) {
data.max_redemptions_allowed = 1;
}
@ -106,7 +103,7 @@ export default Controller.extend(
this.rollbackBuffer();
this.setAutogenerated(opts.autogenerated);
if (!this.autogenerated) {
if (this.type === "email" && opts.sendEmail) {
if (this.isEmail && opts.sendEmail) {
this.send("closeModal");
} else {
this.appEvents.trigger("modal-body:flash", {
@ -126,9 +123,6 @@ export default Controller.extend(
);
},
isLink: equal("type", "link"),
isEmail: equal("type", "email"),
@discourseComputed(
"currentUser.staff",
"siteSettings.invite_link_max_redemptions_limit",
@ -156,46 +150,16 @@ export default Controller.extend(
return staff || groups.any((g) => g.owner);
},
@discourseComputed("type", "buffered.email")
disabled(type, email) {
if (type === "email") {
return !email;
}
return false;
},
@discourseComputed("buffered.hasBufferedChanges", "invite.email", "type")
changed(hasBufferedChanges, inviteEmail, type) {
return hasBufferedChanges || (inviteEmail ? "email" : "link") !== type;
},
@discourseComputed("currentUser.staff", "type")
hasAdvanced(staff, type) {
return staff || type === "email";
@discourseComputed("currentUser.staff", "isEmail", "canInviteToGroup")
hasAdvanced(staff, isEmail, canInviteToGroup) {
return staff || isEmail || canInviteToGroup;
},
@action
copied() {
if (this.type === "email" && !this.buffered.get("email")) {
return this.appEvents.trigger("modal-body:flash", {
text: I18n.t("user.invited.invite.blank_email"),
messageClass: "error",
});
}
this.save({ sendEmail: false, copy: true });
},
@action
toggleLimitToEmail() {
const limitToEmail = !this.limitToEmail;
this.setProperties({
limitToEmail,
type: limitToEmail ? "email" : "link",
});
},
@action
saveInvite(sendEmail) {
this.appEvents.trigger("modal-body:clearFlash");

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
@ -7,7 +7,7 @@ import discourseComputed from "discourse-common/utils/decorators";
// Modal that displays confirmation text when user deletes a topic
// The modal will display only if the topic exceeds a certain amount of views
export default Controller.extend(ModalFunctionality, {
topicController: inject("topic"),
topicController: controller("topic"),
deletingTopic: false,
@discourseComputed("deletingTopic")

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
// Just add query params here to have them automatically passed to topic list filters.
export const queryParams = {
@ -17,7 +17,7 @@ export const queryParams = {
// Basic controller options
const controllerOpts = {
discoveryTopics: inject("discovery/topics"),
discoveryTopics: controller("discovery/topics"),
queryParams: Object.keys(queryParams),
};
@ -27,22 +27,21 @@ controllerOpts.queryParams.forEach((p) => {
});
export function changeSort(sortBy) {
let { controller } = this;
let model = this.controllerFor("discovery.topics").model;
if (sortBy === controller.order) {
controller.toggleProperty("ascending");
model.updateSortParams(sortBy, controller.ascending);
if (sortBy === this.controller.order) {
this.controller.toggleProperty("ascending");
model.updateSortParams(sortBy, this.controller.ascending);
} else {
controller.setProperties({ order: sortBy, ascending: false });
this.controller.setProperties({ order: sortBy, ascending: false });
model.updateSortParams(sortBy, false);
}
}
export function resetParams(skipParams = []) {
let { controller } = this;
controllerOpts.queryParams.forEach((p) => {
if (!skipParams.includes(p)) {
controller.set(p, queryParams[p].default);
this.controller.set(p, queryParams[p].default);
}
});
}

View File

@ -18,7 +18,6 @@ import discourseComputed from "discourse-common/utils/decorators";
import { endWith } from "discourse/lib/computed";
import { routeAction } from "discourse/helpers/route-action";
import { inject as service } from "@ember/service";
import showModal from "discourse/lib/show-modal";
import { userPath } from "discourse/lib/url";
const controllerOpts = {
@ -39,6 +38,18 @@ const controllerOpts = {
order: readOnly("model.params.order"),
ascending: readOnly("model.params.ascending"),
selected: null,
@discourseComputed("model.filter", "model.topics.length")
showDismissRead(filter, topicsLength) {
return this._isFilterPage(filter, "unread") && topicsLength > 0;
},
@discourseComputed("model.filter", "model.topics.length")
showResetNew(filter, topicsLength) {
return this._isFilterPage(filter, "new") && topicsLength > 0;
},
actions: {
changeSort() {
deprecated(
@ -98,17 +109,20 @@ const controllerOpts = {
(this.router.currentRoute.queryParams["f"] ||
this.router.currentRoute.queryParams["filter"]) === "tracked";
Topic.resetNew(this.category, !this.noSubcategories, tracked).then(() =>
let topicIds = this.selected
? this.selected.map((topic) => topic.id)
: null;
Topic.resetNew(this.category, !this.noSubcategories, {
tracked,
topicIds,
}).then(() =>
this.send(
"refresh",
tracked ? { skipResettingParams: ["filter", "f"] } : {}
)
);
},
dismissReadPosts() {
showModal("dismiss-read", { title: "topics.bulk.dismiss_read" });
},
},
afterRefresh(filter, list, listModel = list) {
@ -122,32 +136,6 @@ const controllerOpts = {
this.send("loadingComplete");
},
isFilterPage: function (filter, filterType) {
if (!filter) {
return false;
}
return filter.match(new RegExp(filterType + "$", "gi")) ? true : false;
},
@discourseComputed("model.filter", "model.topics.length")
showDismissRead(filter, topicsLength) {
return this.isFilterPage(filter, "unread") && topicsLength > 0;
},
@discourseComputed("model.filter", "model.topics.length")
showResetNew(filter, topicsLength) {
return this.isFilterPage(filter, "new") && topicsLength > 0;
},
@discourseComputed("model.filter", "model.topics.length")
showDismissAtTop(filter, topicsLength) {
return (
(this.isFilterPage(filter, "new") ||
this.isFilterPage(filter, "unread")) &&
topicsLength >= 15
);
},
hasTopics: gt("model.topics.length", 0),
allLoaded: empty("model.more_topics_url"),
latest: endWith("model.filter", "latest"),

View File

@ -14,7 +14,6 @@ export default Controller.extend(ModalFunctionality, {
minutes: null,
seconds: null,
saveDisabled: false,
enabledUntil: null,
showCustomSelect: equal("selectedSlowMode", "custom"),
durationIsSet: or("hours", "minutes", "seconds"),
@ -87,11 +86,27 @@ export default Controller.extend(ModalFunctionality, {
}
},
@discourseComputed("saveDisabled", "durationIsSet", "enabledUntil")
@discourseComputed(
"saveDisabled",
"durationIsSet",
"model.slow_mode_enabled_until"
)
submitDisabled(saveDisabled, durationIsSet, enabledUntil) {
return saveDisabled || !durationIsSet || !enabledUntil;
},
@discourseComputed("model.slow_mode_seconds")
slowModeEnabled(slowModeSeconds) {
return slowModeSeconds && slowModeSeconds !== 0;
},
@discourseComputed("slowModeEnabled")
saveButtonLabel(slowModeEnabled) {
return slowModeEnabled
? "topic.slow_mode_update.update"
: "topic.slow_mode_update.enable";
},
_setFromSeconds(seconds) {
this.setProperties(fromSeconds(seconds));
},
@ -121,7 +136,11 @@ export default Controller.extend(ModalFunctionality, {
this._parseValue(this.seconds)
);
Topic.setSlowMode(this.model.id, seconds, this.enabledUntil)
Topic.setSlowMode(
this.model.id,
seconds,
this.model.slow_mode_enabled_until
)
.catch(popupAjaxError)
.then(() => {
this.set("model.slow_mode_seconds", seconds);

View File

@ -0,0 +1,101 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import EmberObject, { action } from "@ember/object";
import { extractError } from "discourse/lib/ajax-error";
import { reload } from "discourse/helpers/page-reloader";
const UP = "up";
const DOWN = "down";
export default Controller.extend(ModalFunctionality, {
loading: true,
columns: null,
labelKey: null,
onShow() {
ajax("directory-columns.json")
.then((response) => {
this.setProperties({
loading: false,
columns: response.directory_columns
.sort((a, b) => (a.position > b.position ? 1 : -1))
.map((c) => EmberObject.create(c)),
});
})
.catch(extractError);
},
@action
save() {
this.set("loading", true);
const data = {
directory_columns: this.columns.map((c) =>
c.getProperties("id", "enabled", "position")
),
};
ajax("directory-columns.json", { type: "PUT", data })
.then(() => {
reload();
})
.catch((e) => {
this.set("loading", false);
this.flash(extractError(e), "error");
});
},
@action
resetToDefault() {
let resetColumns = this.columns;
resetColumns
.sort((a, b) =>
(a.automatic_position || a.user_field.position + 1000) >
(b.automatic_position || b.user_field.position + 1000)
? 1
: -1
)
.forEach((column, index) => {
column.setProperties({
position: column.automatic_position || index + 1,
enabled: column.automatic,
});
});
this.set("columns", resetColumns);
this.notifyPropertyChange("columns");
},
@action
moveUp(column) {
this._moveColumn(UP, column);
},
@action
moveDown(column) {
this._moveColumn(DOWN, column);
},
_moveColumn(direction, column) {
if (
(direction === UP && column.position === 1) ||
(direction === DOWN && column.position === this.columns.length)
) {
return;
}
const positionOnClick = column.position;
const newPosition =
direction === UP ? positionOnClick - 1 : positionOnClick + 1;
const previousColumn = this.columns.find((c) => c.position === newPosition);
column.set("position", newPosition);
previousColumn.set("position", positionOnClick);
this.set(
"columns",
this.columns.sort((a, b) => (a.position > b.position ? 1 : -1))
);
this.notifyPropertyChange("columns");
},
});

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import EmberObject from "@ember/object";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
@ -8,7 +8,7 @@ import { categoryLinkHTML } from "discourse/helpers/category-link";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend(ModalFunctionality, {
topicController: inject("topic"),
topicController: controller("topic"),
loading: true,
pinnedInCategoryCount: 0,

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import Badge from "discourse/models/badge";
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import I18n from "I18n";
@ -9,7 +9,7 @@ import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
export default Controller.extend(ModalFunctionality, GrantBadgeController, {
topicController: inject("topic"),
topicController: controller("topic"),
loading: true,
saving: false,
selectedBadgeId: null,

View File

@ -22,7 +22,6 @@ export default Controller.extend(ModalFunctionality, {
schedule("afterRender", () => {
const element = document.querySelector(".insert-link");
element.addEventListener("keydown", this.keyDown);
element
@ -57,6 +56,8 @@ export default Controller.extend(ModalFunctionality, {
this.set("searchResults", []);
event.preventDefault();
event.stopPropagation();
} else {
this.send("closeModal");
}
break;
}

View File

@ -29,6 +29,7 @@ export default Controller.extend(
invitedBy: readOnly("model.invited_by"),
email: alias("model.email"),
hiddenEmail: alias("model.hidden_email"),
emailVerifiedByLink: alias("model.email_verified_by_link"),
accountUsername: alias("model.username"),
passwordRequired: notEmpty("accountPassword"),
successMessage: null,
@ -127,14 +128,16 @@ export default Controller.extend(
"rejectedEmails.[]",
"authOptions.email",
"authOptions.email_valid",
"hiddenEmail"
"hiddenEmail",
"emailVerifiedByLink"
)
emailValidation(
email,
rejectedEmails,
externalAuthEmail,
externalAuthEmailValid,
hiddenEmail
hiddenEmail,
emailVerifiedByLink
) {
if (hiddenEmail) {
return EmberObject.create({
@ -157,12 +160,12 @@ export default Controller.extend(
});
}
if (externalAuthEmail) {
if (externalAuthEmail && externalAuthEmailValid) {
const provider = this.createAccount.authProviderDisplayName(
this.get("authOptions.auth_provider")
);
if (externalAuthEmail === email && externalAuthEmailValid) {
if (externalAuthEmail === email) {
return EmberObject.create({
ok: true,
reason: I18n.t("user.email.authenticated", {
@ -179,6 +182,13 @@ export default Controller.extend(
}
}
if (emailVerifiedByLink) {
return EmberObject.create({
ok: true,
reason: I18n.t("user.email.authenticated_by_invite"),
});
}
if (emailValid(email)) {
return EmberObject.create({
ok: true,

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import { alias, equal } from "@ember/object/computed";
import { mergeTopic, movePosts } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
@ -41,7 +41,7 @@ export default Controller.extend(ModalFunctionality, {
];
},
topicController: inject("topic"),
topicController: controller("topic"),
selectedPostsCount: alias("topicController.selectedPostsCount"),
selectedAllPosts: alias("topicController.selectedAllPosts"),
selectedPosts: alias("topicController.selectedPosts"),

View File

@ -1,6 +1,6 @@
import NavigationDefaultController from "discourse/controllers/navigation/default";
import { inject } from "@ember/controller";
import { inject as controller } from "@ember/controller";
export default NavigationDefaultController.extend({
discoveryCategories: inject("discovery/categories"),
discoveryCategories: controller("discovery/categories"),
});

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import {
iOSWithVisualViewport,
isiPad,
@ -34,7 +34,7 @@ export default Controller.extend({
currentThemeId: -1,
previewingColorScheme: false,
selectedDarkColorSchemeId: null,
preferencesController: inject("preferences"),
preferencesController: controller("preferences"),
makeColorSchemeDefault: true,
init() {

View File

@ -1,8 +1,6 @@
import discourseComputed, { on } from "discourse-common/utils/decorators";
import BufferedMixin from "ember-buffered-proxy/mixin";
import BufferedProxy from "ember-buffered-proxy/proxy";
import Controller from "@ember/controller";
import EmberObjectProxy from "@ember/object/proxy";
import Evented from "@ember/object/evented";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
@ -17,8 +15,7 @@ export default Controller.extend(ModalFunctionality, Evented, {
@discourseComputed("site.categories.[]")
categoriesBuffered(categories) {
const bufProxy = EmberObjectProxy.extend(BufferedMixin || BufferedProxy);
return (categories || []).map((c) => bufProxy.create({ content: c }));
return (categories || []).map((c) => BufferedProxy.create({ content: c }));
},
categoriesOrdered: sort("categoriesBuffered", "categoriesSorting"),

View File

@ -2,6 +2,7 @@ import Controller from "@ember/controller";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isPresent } from "@ember/utils";
import { next } from "@ember/runloop";
export default Controller.extend({
queryParams: [
@ -93,6 +94,10 @@ export default Controller.extend({
this.setProperties(range);
},
refreshModel() {
next(() => this.send("refreshRoute"));
},
actions: {
remove(ids) {
if (!ids) {
@ -104,7 +109,7 @@ export default Controller.extend({
});
if (newList.length === 0) {
this.send("refreshRoute");
this.refreshModel();
} else {
this.set("reviewables", newList);
}
@ -112,7 +117,7 @@ export default Controller.extend({
resetTopic() {
this.set("topic_id", null);
this.send("refreshRoute");
this.refreshModel();
},
refresh() {
@ -165,7 +170,7 @@ export default Controller.extend({
additional_filters: JSON.stringify(this.additionalFilters),
});
this.send("refreshRoute");
this.refreshModel();
},
loadMore() {

View File

@ -87,7 +87,7 @@ export default Controller.extend(ModalFunctionality, {
attestation: "none",
authenticatorSelection: {
// see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why
// default value of preferred is not necesarrily what we want, it limits webauthn to only devices that support
// default value of preferred is not necessarily what we want, it limits webauthn to only devices that support
// user verification, which usually requires entering a PIN
userVerification: "discouraged",
},

View File

@ -8,7 +8,7 @@ export default Controller.extend({
const tagGroups = this.tagGroups.model;
tagGroups.pushObject(this.model);
this.transitionToRoute("tagGroups.edit", this.model);
this.transitionToRoute("tagGroups.index");
},
},
});

View File

@ -8,7 +8,6 @@ import Topic from "discourse/models/topic";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import { queryParams } from "discourse/controllers/discovery-sortable";
import showModal from "discourse/lib/show-modal";
export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
application: controller(),
@ -93,48 +92,31 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
}
},
isFilterPage: function (filter, filterType) {
if (!filter) {
return false;
}
return filter.match(new RegExp(filterType + "$", "gi")) ? true : false;
},
@discourseComputed("list.filter", "list.topics.length")
showDismissRead(filter, topicsLength) {
return this.isFilterPage(filter, "unread") && topicsLength > 0;
return this._isFilterPage(filter, "unread") && topicsLength > 0;
},
@discourseComputed("list.filter", "list.topics.length")
showResetNew(filter, topicsLength) {
return this.isFilterPage(filter, "new") && topicsLength > 0;
},
@discourseComputed("list.filter", "list.topics.length")
showDismissAtTop(filter, topicsLength) {
return (
(this.isFilterPage(filter, "new") ||
this.isFilterPage(filter, "unread")) &&
topicsLength >= 15
);
return this._isFilterPage(filter, "new") && topicsLength > 0;
},
actions: {
dismissReadPosts() {
showModal("dismiss-read", { title: "topics.bulk.dismiss_read" });
},
resetNew() {
const tracked =
(this.router.currentRoute.queryParams["f"] ||
this.router.currentRoute.queryParams["filter"]) === "tracked";
Topic.resetNew(
this.category,
!this.noSubcategories,
let topicIds = this.selected
? this.selected.map((topic) => topic.id)
: null;
Topic.resetNew(this.category, !this.noSubcategories, {
tracked,
this.tag
).then(() =>
tag: this.tag,
topicIds,
}).then(() =>
this.send(
"refresh",
tracked ? { skipResettingParams: ["filter", "f"] } : {}

View File

@ -1036,7 +1036,8 @@ export default Controller.extend(bufferedProperty("model"), {
options = {
action: Composer.CREATE_TOPIC,
draftKey: post.topic.draft_key,
categoryId: this.get("model.category.id"),
topicCategoryId: this.get("model.category.id"),
prioritizedCategoryId: this.get("model.category.id"),
};
}
@ -1606,7 +1607,7 @@ export default Controller.extend(bufferedProperty("model"), {
}
// scroll to bottom is very specific to new posts from discobot
// hence the -2 check (dicobot id). We can shift all this code
// hence the -2 check (discobot id). We can shift all this code
// to discobot plugin longer term
if (
topic.get("isPrivateMessage") &&
@ -1639,7 +1640,7 @@ export default Controller.extend(bufferedProperty("model"), {
function () {
const $post = $(`.topic-post article#post_${postNumber}`);
if ($post.length === 0 || isElementInViewport($post)) {
if ($post.length === 0 || isElementInViewport($post[0])) {
return;
}

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import Bookmark from "discourse/models/bookmark";
import I18n from "I18n";
import { Promise } from "rsvp";
@ -6,8 +6,8 @@ import EmberObject, { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
application: inject(),
user: inject(),
application: controller(),
user: controller(),
content: null,
loading: false,

View File

@ -1,14 +1,27 @@
import Controller, { inject as controller } from "@ember/controller";
import { alias, sort } from "@ember/object/computed";
import { action, computed } from "@ember/object";
import { alias, filterBy, sort } from "@ember/object/computed";
export default Controller.extend({
user: controller(),
username: alias("user.model.username_lower"),
sortedBadges: sort("model", "badgeSortOrder"),
favoriteBadges: filterBy("model", "is_favorite", true),
canFavoriteMoreBadges: computed(
"favoriteBadges.length",
"model.meta.max_favorites",
function () {
return this.favoriteBadges.length < this.model.meta.max_favorites;
}
),
init() {
this._super(...arguments);
this.badgeSortOrder = ["badge.badge_type.sort_order:desc", "badge.name"];
},
@action
favorite(badge) {
return badge.favorite();
},
});

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { durationTiny } from "discourse/lib/formatter";
@ -7,7 +7,7 @@ import { durationTiny } from "discourse/lib/formatter";
const MAX_BADGES = 6;
export default Controller.extend({
userController: inject("user"),
userController: controller("user"),
user: alias("userController.model"),
@discourseComputed("model.badges.length")

View File

@ -1,4 +1,4 @@
import Controller, { inject } from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { computed, set } from "@ember/object";
import { and, equal, gt, not, or } from "@ember/object/computed";
import CanCheckEmails from "discourse/mixins/can-check-emails";
@ -15,7 +15,7 @@ import { inject as service } from "@ember/service";
export default Controller.extend(CanCheckEmails, {
router: service(),
userNotifications: inject("user-notifications"),
userNotifications: controller("user-notifications"),
adminTools: optionalService(),
@discourseComputed("model.username")

View File

@ -1,6 +1,7 @@
import Controller, { inject as controller } from "@ember/controller";
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 { longDate } from "discourse/lib/formatter";
import { observes } from "discourse-common/utils/decorators";
@ -9,13 +10,14 @@ export default Controller.extend({
application: controller(),
queryParams: ["period", "order", "asc", "name", "group", "exclude_usernames"],
period: "weekly",
order: "likes_received",
order: "",
asc: null,
name: "",
group: null,
nameInput: null,
exclude_usernames: null,
isLoading: false,
columns: null,
showTimeRead: equal("period", "all"),
@ -23,9 +25,15 @@ export default Controller.extend({
this.set("isLoading", true);
this.set("nameInput", params.name);
this.set("order", params.order);
const custom_field_columns = this.columns.filter((c) => !c.automatic);
const user_field_ids = custom_field_columns
.map((c) => c.user_field_id)
.join("|");
this.store
.find("directoryItem", params)
.find("directoryItem", Object.assign(params, { user_field_ids }))
.then((model) => {
const lastUpdatedAt = model.get("resultSetMeta.last_updated_at");
this.setProperties({
@ -39,6 +47,11 @@ export default Controller.extend({
});
},
@action
showEditColumnsModal() {
showModal("edit-user-directory-columns");
},
@action
onFilterChanged(filter) {
discourseDebounce(this, this._setName, filter, 500);

View File

@ -0,0 +1,10 @@
import { htmlSafe } from "@ember/template";
import { registerUnbound } from "discourse-common/lib/helpers";
import I18n from "I18n";
export default registerUnbound("mobile-directory-item-label", function (args) {
// Args should include key/values { item, column }
const count = args.item.get(args.column.name);
return htmlSafe(I18n.t(`directory.${args.column.name}`, { count }));
});

View File

@ -0,0 +1,16 @@
import { htmlSafe } from "@ember/template";
import { registerUnbound } from "discourse-common/lib/helpers";
export default registerUnbound(
"directory-item-user-field-value",
function (args) {
// Args should include key/values { item, column }
const value =
args.item.user && args.item.user.user_fields
? args.item.user.user_fields[args.column.user_field_id]
: null;
const content = value || "-";
return htmlSafe(`<span class='user-field-value'>${content}</span>`);
}
);

View File

@ -0,0 +1,11 @@
import { htmlSafe } from "@ember/template";
import { registerUnbound } from "discourse-common/lib/helpers";
import { number } from "discourse/lib/formatter";
export default registerUnbound("directory-item-value", function (args) {
// Args should include key/values { item, column }
return htmlSafe(
`<span class='number'>${number(args.item.get(args.column.name))}</span>`
);
});

View File

@ -0,0 +1,19 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import I18n from "I18n";
import { iconHTML } from "discourse-common/lib/icon-library";
import { htmlSafe } from "@ember/template";
export default registerUnbound("directory-table-header-title", function (args) {
// Args should include key/values { field, labelKey, icon, translated }
let html = "";
if (args.icon) {
html += iconHTML(args.icon);
}
let labelKey = args.labelKey || `directory.${args.field}`;
html += args.translated
? args.field
: I18n.t(labelKey + "_long", { defaultValue: I18n.t(labelKey) });
return htmlSafe(html);
});

View File

@ -59,7 +59,7 @@ function renderAvatar(user, options) {
const description = get(user, "description");
// if a description has been provided
if (description && description.length > 0) {
// preprend the username before the description
// prepend the username before the description
title = I18n.t("user.avatar.name_and_description", {
name: displayName,
description,

View File

@ -25,9 +25,6 @@
<section id='main'>
</section>
<div id='offscreen-content'>
</div>
<bootstrap-content key="hidden-login-form">
<bootstrap-content key="preloaded">

View File

@ -1,4 +1,4 @@
// Updates the PWA badging if avaliable
// Updates the PWA badging if available
export default {
name: "badging",
after: "message-bus",

View File

@ -2,7 +2,7 @@ import { isProduction, isTesting } from "discourse-common/config/environment";
// Initialize the message bus to receive messages.
import getURL from "discourse-common/lib/get-url";
import { handleLogoff } from "discourse/lib/ajax";
import userPresent from "discourse/lib/user-presence";
import userPresent, { onPresenceChange } from "discourse/lib/user-presence";
const LONG_POLL_AFTER_UNSEEN_TIME = 1200000; // 20 minutes
const CONNECTIVITY_ERROR_CLASS = "message-bus-offline";
@ -51,6 +51,18 @@ export default {
// we do not want to start anything till document is complete
messageBus.stop();
// This will notify MessageBus to force a long poll after user becomes
// present
// When 20 minutes pass we stop long polling due to "shouldLongPollCallback".
onPresenceChange({
unseenTime: LONG_POLL_AFTER_UNSEEN_TIME,
callback: () => {
if (messageBus.onVisibilityChange) {
messageBus.onVisibilityChange();
}
},
});
if (siteSettings.login_required && !user) {
// Endpoint is not available in this case, so don't try
return;

View File

@ -88,6 +88,7 @@ export default {
const oneboxTypes = {
amazon: "discourse-amazon",
githubactions: "fab-github",
githubblob: "fab-github",
githubcommit: "fab-github",
githubpullrequest: "fab-github",

View File

@ -73,17 +73,19 @@ export default {
);
if (staleIndex === -1) {
// this gets a bit tricky, unread pms are bumped to front
// high priority and unread notifications are first
let insertPosition = 0;
if (lastNotification.notification_type !== 6) {
insertPosition = oldNotifications.findIndex(
(n) => n.notification_type !== 6 || n.read
if (!lastNotification.high_priority || lastNotification.read) {
const nextPosition = oldNotifications.findIndex(
(n) => !n.high_priority || n.read
);
insertPosition =
insertPosition === -1
? oldNotifications.length - 1
: insertPosition;
if (nextPosition !== -1) {
insertPosition = nextPosition;
}
}
oldNotifications.insertAt(
insertPosition,
EmberObject.create(lastNotification)

View File

@ -9,8 +9,9 @@ export default {
const currentUser = container.lookup("current-user:main");
if (currentUser) {
const username = currentUser.get("username");
const escapedUsername = username.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
DiscourseURL.rewrite(
new RegExp(`^/u/${username}/?$`, "i"),
new RegExp(`^/u/${escapedUsername}/?$`, "i"),
`/u/${username}/activity`
);
}

View File

@ -70,6 +70,12 @@ export function ajax() {
args = arguments[1];
}
let ignoreUnsent = true;
if (args.ignoreUnsent !== undefined) {
ignoreUnsent = args.ignoreUnsent;
delete args.ignoreUnsent;
}
function performAjax(resolve, reject) {
args.headers = args.headers || {};
@ -112,7 +118,7 @@ export function ajax() {
args.error = (xhr, textStatus, errorThrown) => {
// 0 represents the `UNSENT` state
if (xhr.readyState === 0) {
if (ignoreUnsent && xhr.readyState === 0) {
// Make sure we log pretender errors in test mode
if (textStatus === "error" && isTesting()) {
throw errorThrown;
@ -128,7 +134,7 @@ export function ajax() {
Session.current().set("csrfToken", null);
}
// If it's a parsererror, don't reject
// If it's a parser error, don't reject
if (xhr.status === 200) {
return args.success(xhr);
}
@ -162,10 +168,6 @@ export function ajax() {
args.headers["Discourse-Script"] = true;
}
if (args.type === "GET" && args.cache !== true) {
args.cache = true; // Disable JQuery cache busting param, which was created to deal with IE8
}
ajaxObj = $.ajax(getURL(url), args);
}

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