Version bump
This commit is contained in:
commit
d0e09c512c
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@ -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. -->
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -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
59
COPYRIGHT.md
Normal 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.
|
||||
```
|
||||
@ -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)
|
||||
8
Gemfile
8
Gemfile
@ -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'
|
||||
|
||||
73
Gemfile.lock
73
Gemfile.lock
@ -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
|
||||
|
||||
14
LICENSE.txt
14
LICENSE.txt
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1 +1,9 @@
|
||||
{{d-icon "times"}} {{word.word}} {{#if word.replacement}}→ <span class="replacement">{{word.replacement}}</span>{{/if}}
|
||||
{{d-icon "times"}} {{word.word}}
|
||||
{{#if (or isReplace isLink)}}
|
||||
→ <span class="replacement">{{word.replacement}}</span>
|
||||
{{else if isTag}}
|
||||
→
|
||||
{{#each tags as |tag|}}
|
||||
<span class="tag">{{tag}}</span>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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>
|
||||
→
|
||||
<span class="replacement">{{match.replacement}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{else if isTag}}
|
||||
{{#each matches as |match|}}
|
||||
<li>
|
||||
<span class="match">{{match.match}}</span>
|
||||
→
|
||||
{{#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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -31,4 +31,9 @@ export default Component.extend({
|
||||
}
|
||||
return sanitize(description);
|
||||
},
|
||||
|
||||
@discourseComputed("badge.id")
|
||||
showFavorite(badgeId) {
|
||||
return ![1, 2, 3, 4].includes(badgeId);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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",
|
||||
});
|
||||
|
||||
@ -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])?(?:\\))?",
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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"
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -5,4 +5,5 @@ export default Component.extend({
|
||||
tagName: "tr",
|
||||
classNameBindings: ["me"],
|
||||
me: propertyEqual("item.user.id", "currentUser.id"),
|
||||
columns: null,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -24,7 +24,6 @@ export default Component.extend({
|
||||
date: datetime.format("YYYY-MM-DD"),
|
||||
time: datetime.format("HH:mm"),
|
||||
});
|
||||
this._updateInput();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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));
|
||||
},
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
},
|
||||
});
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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`;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -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",
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: "",
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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"),
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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",
|
||||
},
|
||||
|
||||
@ -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");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"] } : {}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 }));
|
||||
});
|
||||
@ -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>`);
|
||||
}
|
||||
);
|
||||
@ -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>`
|
||||
);
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -25,9 +25,6 @@
|
||||
<section id='main'>
|
||||
</section>
|
||||
|
||||
<div id='offscreen-content'>
|
||||
</div>
|
||||
|
||||
<bootstrap-content key="hidden-login-form">
|
||||
<bootstrap-content key="preloaded">
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Updates the PWA badging if avaliable
|
||||
// Updates the PWA badging if available
|
||||
export default {
|
||||
name: "badging",
|
||||
after: "message-bus",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -88,6 +88,7 @@ export default {
|
||||
|
||||
const oneboxTypes = {
|
||||
amazon: "discourse-amazon",
|
||||
githubactions: "fab-github",
|
||||
githubblob: "fab-github",
|
||||
githubcommit: "fab-github",
|
||||
githubpullrequest: "fab-github",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
Reference in New Issue
Block a user