Merge branch 'master' into master
This commit is contained in:
commit
810176be5b
|
@ -2,7 +2,7 @@
|
|||
"maxReviewers": 2,
|
||||
"message": "Thanks @pullRequester for your pull request :+1:. By analyzing the blame information on this pull request, I identified @reviewers to be potential reviewers.",
|
||||
"requiredOrgs": ["discourse"],
|
||||
"skipCollaboratorPR": true,
|
||||
"delayed": true,
|
||||
"delayedUntil": "6d"
|
||||
"skipCollaboratorPR": false,
|
||||
"delayed": false,
|
||||
"delayedUntil": "1d"
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ before_install:
|
|||
- git clone --depth=1 https://github.com/discourse/discourse-backup-uploads-to-s3.git plugins/discourse-backup-uploads-to-s3
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-spoiler-alert.git plugins/discourse-spoiler-alert
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
|
||||
- npm i -g eslint babel-eslint
|
||||
- eslint app/assets/javascripts
|
||||
- eslint --ext .es6 app/assets/javascripts
|
||||
|
@ -52,7 +53,7 @@ before_script:
|
|||
- bundle exec rake db:create db:migrate
|
||||
|
||||
install:
|
||||
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails rails-observers seed-fu; fi"
|
||||
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi"
|
||||
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
|
||||
|
||||
script: "bundle exec rspec && bundle exec rake plugin:spec && bundle exec rake qunit:test['200000']"
|
||||
|
|
37
Gemfile
37
Gemfile
|
@ -9,7 +9,6 @@ end
|
|||
if rails_master?
|
||||
gem 'arel', git: 'https://github.com/rails/arel.git'
|
||||
gem 'rails', git: 'https://github.com/rails/rails.git'
|
||||
gem 'rails-observers', git: 'https://github.com/rails/rails-observers.git'
|
||||
gem 'seed-fu', git: 'https://github.com/SamSaffron/seed-fu.git', branch: 'discourse'
|
||||
else
|
||||
# Rails 5 is going to ship with Action Cable, we have no use for it as
|
||||
|
@ -29,8 +28,6 @@ else
|
|||
# gem 'railties'
|
||||
# gem 'sprockets-rails'
|
||||
gem 'rails', '~> 4.2'
|
||||
|
||||
gem 'rails-observers'
|
||||
gem 'seed-fu', '~> 2.3.5'
|
||||
end
|
||||
|
||||
|
@ -48,7 +45,8 @@ gem 'onebox'
|
|||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
gem 'ember-rails', '0.18.5'
|
||||
gem 'ember-source', '2.4.6'
|
||||
gem 'ember-source', '2.10.0'
|
||||
gem 'ember-handlebars-template', '0.7.5'
|
||||
gem 'barber'
|
||||
gem 'babel-transpiler'
|
||||
|
||||
|
@ -107,7 +105,6 @@ gem 'sidekiq-statistic'
|
|||
gem 'sinatra', require: false
|
||||
gem 'execjs', require: false
|
||||
gem 'mini_racer'
|
||||
gem 'thin', require: false
|
||||
gem 'highline', require: false
|
||||
gem 'rack-protection' # security
|
||||
|
||||
|
@ -138,8 +135,6 @@ group :test, :development do
|
|||
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
|
||||
gem 'rspec-rails', require: false
|
||||
gem 'shoulda', require: false
|
||||
gem 'simplecov', require: false
|
||||
gem 'rspec-given'
|
||||
gem 'rspec-html-matchers'
|
||||
gem 'spork-rails'
|
||||
gem 'pry-nav'
|
||||
|
@ -168,44 +163,22 @@ gem 'htmlentities', require: false
|
|||
# If you want to amend mini profiler to do the monkey patches in the railties
|
||||
# we are open to it. by deferring require to the initializer we can configure discourse installs without it
|
||||
|
||||
gem 'fast_stack', require: false, platform: [:mri_20]
|
||||
gem 'flamegraph', require: false
|
||||
gem 'rack-mini-profiler', require: false
|
||||
|
||||
gem 'unicorn', require: false
|
||||
gem 'puma', require: false
|
||||
gem 'rbtrace', require: false, platform: :mri
|
||||
gem 'gc_tracer', require: false, platform: :mri
|
||||
|
||||
# required for feed importing and embedding
|
||||
#
|
||||
gem 'ruby-readability', require: false
|
||||
|
||||
gem 'simple-rss', require: false
|
||||
|
||||
gem 'gctools', require: false, platform: :mri_21
|
||||
|
||||
begin
|
||||
gem 'stackprof', require: false, platform: [:mri_21, :mri_22, :mri_23]
|
||||
gem 'memory_profiler', require: false, platform: [:mri_21, :mri_22, :mri_23]
|
||||
rescue Bundler::GemfileError
|
||||
begin
|
||||
STDERR.puts "You are running an old version of bundler, please upgrade bundler ASAP, if you are using Discourse docker, rebuild your container."
|
||||
gem 'stackprof', require: false, platform: [:mri_21, :mri_22]
|
||||
gem 'memory_profiler', require: false, platform: [:mri_21, :mri_22]
|
||||
rescue Bundler::GemfileError
|
||||
gem 'stackprof', require: false, platform: [:mri_21]
|
||||
gem 'memory_profiler', require: false, platform: [:mri_21]
|
||||
end
|
||||
end
|
||||
gem 'stackprof', require: false, platform: :mri
|
||||
gem 'memory_profiler', require: false, platform: :mri
|
||||
|
||||
gem 'rmmseg-cpp', require: false
|
||||
|
||||
gem 'logster'
|
||||
|
||||
# perftools only works on 1.9 atm
|
||||
group :profile do
|
||||
# travis refuses to install this, instead of fuffing, just avoid it for now
|
||||
#
|
||||
# if you need to profile, uncomment out this line
|
||||
# gem 'rack-perftools_profiler', require: 'rack/perftools_profiler', platform: :mri_19
|
||||
end
|
||||
|
|
69
Gemfile.lock
69
Gemfile.lock
|
@ -52,7 +52,7 @@ GEM
|
|||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
execjs (~> 2.0)
|
||||
barber (0.11.1)
|
||||
barber (0.11.2)
|
||||
ember-source (>= 1.0, < 3)
|
||||
execjs (>= 1.2, < 3)
|
||||
better_errors (2.1.1)
|
||||
|
@ -71,19 +71,17 @@ GEM
|
|||
concurrent-ruby (1.0.2)
|
||||
connection_pool (2.2.0)
|
||||
crass (1.0.2)
|
||||
daemons (1.2.4)
|
||||
debug_inspector (0.0.2)
|
||||
diff-lcs (1.2.5)
|
||||
discourse-qunit-rails (0.0.9)
|
||||
railties
|
||||
discourse_fastimage (2.0.3)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
email_reply_trimmer (0.1.6)
|
||||
ember-data-source (2.2.1)
|
||||
ember-source (>= 1.8, < 3.0)
|
||||
ember-handlebars-template (0.7.4)
|
||||
ember-handlebars-template (0.7.5)
|
||||
barber (>= 0.11.0)
|
||||
sprockets (>= 3.3, < 4)
|
||||
ember-rails (0.18.5)
|
||||
|
@ -93,9 +91,8 @@ GEM
|
|||
ember-source (>= 1.1.0)
|
||||
jquery-rails (>= 1.0.17)
|
||||
railties (>= 3.1)
|
||||
ember-source (2.4.6)
|
||||
ember-source (2.10.0)
|
||||
erubis (2.7.0)
|
||||
eventmachine (1.2.0.1)
|
||||
excon (0.53.0)
|
||||
execjs (2.7.0)
|
||||
exifr (1.2.4)
|
||||
|
@ -104,23 +101,20 @@ GEM
|
|||
faraday (0.9.2)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fast_stack (0.2.0)
|
||||
fast_xor (1.1.3)
|
||||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
ffi (1.9.10)
|
||||
ffi (1.9.17)
|
||||
flamegraph (0.9.5)
|
||||
foreman (0.82.0)
|
||||
thor (~> 0.19.1)
|
||||
fspath (2.1.1)
|
||||
gctools (0.2.3)
|
||||
given_core (3.7.1)
|
||||
sorcerer (>= 0.3.7)
|
||||
gc_tracer (1.5.1)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
guess_html_encoding (0.0.11)
|
||||
hashie (3.4.4)
|
||||
hashie (3.4.6)
|
||||
highline (1.7.8)
|
||||
hiredis (0.6.1)
|
||||
htmlentities (4.3.4)
|
||||
|
@ -141,10 +135,10 @@ GEM
|
|||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.3)
|
||||
json (1.8.6)
|
||||
jwt (1.5.2)
|
||||
kgio (2.10.0)
|
||||
libv8 (5.3.332.38.1)
|
||||
libv8 (5.3.332.38.3)
|
||||
listen (0.7.3)
|
||||
logster (1.2.5)
|
||||
loofah (2.0.3)
|
||||
|
@ -152,7 +146,7 @@ GEM
|
|||
lru_redux (1.1.0)
|
||||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memory_profiler (0.9.6)
|
||||
memory_profiler (0.9.7)
|
||||
message_bus (2.0.2)
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
|
@ -165,8 +159,8 @@ GEM
|
|||
mocha (1.1.0)
|
||||
metaclass (~> 0.0.1)
|
||||
mock_redis (0.15.4)
|
||||
moneta (0.8.0)
|
||||
msgpack (0.7.6)
|
||||
moneta (0.8.1)
|
||||
msgpack (1.0.2)
|
||||
multi_json (1.12.1)
|
||||
multi_xml (0.5.5)
|
||||
multipart-post (2.0.0)
|
||||
|
@ -176,7 +170,7 @@ GEM
|
|||
mini_portile2 (~> 2.1.0)
|
||||
nokogumbo (1.4.7)
|
||||
nokogiri
|
||||
oauth (0.4.7)
|
||||
oauth (0.5.1)
|
||||
oauth2 (1.0.0)
|
||||
faraday (>= 0.8, < 0.10)
|
||||
jwt (~> 1.0)
|
||||
|
@ -209,15 +203,17 @@ GEM
|
|||
omniauth-openid (1.0.1)
|
||||
omniauth (~> 1.0)
|
||||
rack-openid (~> 1.3.1)
|
||||
omniauth-twitter (1.2.1)
|
||||
json (~> 1.3)
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
onebox (1.6.2)
|
||||
rack
|
||||
onebox (1.7.7)
|
||||
fast_blank (>= 1.0.0)
|
||||
htmlentities (~> 4.3.4)
|
||||
moneta (~> 0.8)
|
||||
multi_json (~> 1.11)
|
||||
mustache
|
||||
nokogiri (~> 1.6.6)
|
||||
sanitize
|
||||
openid-redis-store (0.0.2)
|
||||
redis
|
||||
ruby-openid
|
||||
|
@ -233,7 +229,7 @@ GEM
|
|||
pry (>= 0.9.10)
|
||||
puma (3.6.0)
|
||||
r2 (0.2.6)
|
||||
rack (1.6.4)
|
||||
rack (1.6.5)
|
||||
rack-mini-profiler (0.10.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-openid (1.3.1)
|
||||
|
@ -262,8 +258,6 @@ GEM
|
|||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-observers (0.1.2)
|
||||
activemodel (~> 4.0)
|
||||
rails_multisite (1.0.6)
|
||||
rails (> 4.2, < 5)
|
||||
railties (4.2.7.1)
|
||||
|
@ -300,9 +294,6 @@ GEM
|
|||
rspec-expectations (3.4.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-given (3.7.1)
|
||||
given_core (= 3.7.1)
|
||||
rspec (>= 2.14.0)
|
||||
rspec-html-matchers (0.7.0)
|
||||
nokogiri (~> 1)
|
||||
rspec (~> 3)
|
||||
|
@ -340,7 +331,7 @@ GEM
|
|||
shoulda (3.5.0)
|
||||
shoulda-context (~> 1.0, >= 1.0.1)
|
||||
shoulda-matchers (>= 1.4.1, < 3.0)
|
||||
shoulda-context (1.2.1)
|
||||
shoulda-context (1.2.2)
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (4.2.4)
|
||||
|
@ -351,17 +342,11 @@ GEM
|
|||
sidekiq-statistic (1.2.0)
|
||||
sidekiq (>= 3.3.4, < 5)
|
||||
simple-rss (1.3.1)
|
||||
simplecov (0.11.1)
|
||||
docile (~> 1.1.0)
|
||||
json (~> 1.8)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
sinatra (1.4.6)
|
||||
rack (~> 1.4)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (>= 1.3, < 3)
|
||||
slop (3.6.0)
|
||||
sorcerer (1.0.2)
|
||||
spork (1.0.0rc4)
|
||||
spork-rails (4.0.0)
|
||||
rails (>= 3.0.0, < 5)
|
||||
|
@ -374,10 +359,6 @@ GEM
|
|||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
stackprof (0.2.10)
|
||||
thin (1.7.0)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
eventmachine (~> 1.0, >= 1.0.4)
|
||||
rack (>= 1, < 3)
|
||||
thor (0.19.1)
|
||||
thread_safe (0.3.5)
|
||||
tilt (2.0.5)
|
||||
|
@ -412,19 +393,19 @@ DEPENDENCIES
|
|||
discourse-qunit-rails
|
||||
discourse_fastimage (= 2.0.3)
|
||||
email_reply_trimmer (= 0.1.6)
|
||||
ember-handlebars-template (= 0.7.5)
|
||||
ember-rails (= 0.18.5)
|
||||
ember-source (= 2.4.6)
|
||||
ember-source (= 2.10.0)
|
||||
excon
|
||||
execjs
|
||||
fabrication (= 2.9.8)
|
||||
fakeweb (~> 1.3.0)
|
||||
fast_blank
|
||||
fast_stack
|
||||
fast_xor
|
||||
fast_xs
|
||||
flamegraph
|
||||
foreman
|
||||
gctools
|
||||
gc_tracer
|
||||
highline
|
||||
hiredis
|
||||
htmlentities
|
||||
|
@ -463,7 +444,6 @@ DEPENDENCIES
|
|||
rack-mini-profiler
|
||||
rack-protection
|
||||
rails (~> 4.2)
|
||||
rails-observers
|
||||
rails_multisite
|
||||
rake
|
||||
rb-fsevent
|
||||
|
@ -475,7 +455,6 @@ DEPENDENCIES
|
|||
rinku
|
||||
rmmseg-cpp
|
||||
rspec
|
||||
rspec-given
|
||||
rspec-html-matchers
|
||||
rspec-rails
|
||||
rtlit
|
||||
|
@ -488,15 +467,13 @@ DEPENDENCIES
|
|||
sidekiq
|
||||
sidekiq-statistic
|
||||
simple-rss
|
||||
simplecov
|
||||
sinatra
|
||||
spork-rails
|
||||
stackprof
|
||||
thin
|
||||
timecop
|
||||
uglifier
|
||||
unf
|
||||
unicorn
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.6
|
||||
1.13.7
|
||||
|
|
10
README.md
10
README.md
|
@ -11,11 +11,11 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
|
|||
## Screenshots
|
||||
|
||||
<a href="https://bbs.boingboing.net"><img src="https://www.discourse.org/faq/14/boing-boing-discourse.png" width="720px"></a>
|
||||
<a href="https://discuss.newrelic.com/"><img src="https://www.discourse.org/faq/14/new-relic-discourse.png" width="720px"></a>
|
||||
<a href="http://discuss.howtogeek.com"><img src="https://www.discourse.org/faq/14/how-to-geek-discourse.png" width="720px"></a>
|
||||
<a href="https://talk.turtlerockstudios.com/"><img src="https://www.discourse.org/faq/14/turtle-rock-discourse.jpg" width="720px"></a>
|
||||
<a href="https://twittercommunity.com/"><img src="https://www.discourse.org/faq/17/twitter-discourse.png" width="720px"></a>
|
||||
<a href="http://discuss.howtogeek.com"><img src="https://www.discourse.org/faq/17/how-to-geek-discourse.png" width="720px"></a>
|
||||
<a href="https://talk.turtlerockstudios.com/"><img src="https://www.discourse.org/faq/17/turtle-rock-discourse.png" width="720px"></a>
|
||||
|
||||
<a href="https://discuss.atom.io"><img src="https://www.discourse.org/faq/15/nexus-7-2013-mobile-discourse.png?v=2" alt="Atom" width="430px"></a>
|
||||
<a href="https://discuss.atom.io"><img src="https://www.discourse.org/faq/17/nexus-7-2013-mobile-discourse.png" alt="Atom" width="430px"></a>
|
||||
<a href="//discourse.soylent.com"><img src="https://www.discourse.org/faq/15/iphone-5s-mobile-discourse.png" alt="Soylent" width="270px"></a>
|
||||
|
||||
Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).
|
||||
|
@ -84,7 +84,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
|
|||
|
||||
## Copyright / License
|
||||
|
||||
Copyright 2014 - 2016 Civilized Discourse Construction Kit, Inc.
|
||||
Copyright 2014 - 2017 Civilized Discourse Construction Kit, Inc.
|
||||
|
||||
Licensed under the GNU General Public License Version 2.0 (or later);
|
||||
you may not use this work except in compliance with the License.
|
||||
|
|
|
@ -36,6 +36,7 @@ export default Ember.Component.extend({
|
|||
|
||||
loadScript("/javascripts/ace/ace.js", { scriptTag: true }).then(() => {
|
||||
window.ace.require(['ace/ace'], loadedAce => {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) { return; }
|
||||
const editor = loadedAce.edit(this.$('.ace')[0]);
|
||||
|
||||
editor.setTheme("ace/theme/chrome");
|
||||
|
|
|
@ -68,7 +68,7 @@ export default Ember.Controller.extend(BufferedContent, {
|
|||
this.get('model').save(data).then(() => {
|
||||
if (newBadge) {
|
||||
const adminBadges = this.get('adminBadges.model');
|
||||
if (!adminBadges.contains(model)) {
|
||||
if (!adminBadges.includes(model)) {
|
||||
adminBadges.pushObject(model);
|
||||
}
|
||||
this.transitionToRoute('adminBadges.show', model.get('id'));
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { propertyEqual } from 'discourse/lib/computed';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
@ -7,19 +6,6 @@ export default Ember.Controller.extend({
|
|||
disableSave: false,
|
||||
savingStatus: '',
|
||||
|
||||
currentPage: function() {
|
||||
if (this.get("model.user_count") === 0) { return 0; }
|
||||
return Math.floor(this.get("model.offset") / this.get("model.limit")) + 1;
|
||||
}.property("model.limit", "model.offset", "model.user_count"),
|
||||
|
||||
totalPages: function() {
|
||||
if (this.get("model.user_count") === 0) { return 0; }
|
||||
return Math.floor(this.get("model.user_count") / this.get("model.limit")) + 1;
|
||||
}.property("model.limit", "model.user_count"),
|
||||
|
||||
showingFirst: Em.computed.lte("currentPage", 1),
|
||||
showingLast: propertyEqual("currentPage", "totalPages"),
|
||||
|
||||
aliasLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.alias_levels.nobody"), value: 0 },
|
||||
|
@ -47,38 +33,6 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
next() {
|
||||
if (this.get("showingLast")) { return; }
|
||||
|
||||
const group = this.get("model"),
|
||||
offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count"));
|
||||
|
||||
group.set("offset", offset);
|
||||
|
||||
return group.findMembers();
|
||||
},
|
||||
|
||||
previous() {
|
||||
if (this.get("showingFirst")) { return; }
|
||||
|
||||
const group = this.get("model"),
|
||||
offset = Math.max(group.get("offset") - group.get("limit"), 0);
|
||||
|
||||
group.set("offset", offset);
|
||||
|
||||
return group.findMembers();
|
||||
},
|
||||
|
||||
removeMember(member) {
|
||||
const self = this,
|
||||
message = I18n.t("admin.groups.delete_member_confirm", { username: member.get("username"), group: this.get("model.name") });
|
||||
return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) {
|
||||
if (confirm) {
|
||||
self.get("model").removeMember(member);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeOwner(member) {
|
||||
const self = this,
|
||||
message = I18n.t("admin.groups.delete_owner_confirm", { username: member.get("username"), group: this.get("model.name") });
|
||||
|
@ -95,12 +49,6 @@ export default Ember.Controller.extend({
|
|||
this.set("model.ownerUsernames", null);
|
||||
},
|
||||
|
||||
addMembers() {
|
||||
if (Em.isEmpty(this.get("model.usernames"))) { return; }
|
||||
this.get("model").addMembers(this.get("model.usernames")).catch(popupAjaxError);
|
||||
this.set("model.usernames", null);
|
||||
},
|
||||
|
||||
save() {
|
||||
const group = this.get('model'),
|
||||
groupsController = this.get("adminGroupsType"),
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
adminRoutes: function() {
|
||||
return this.get('model').map(p => {
|
||||
if (p.get('enabled')) {
|
||||
return p.admin_route;
|
||||
@computed('model.@each.enabled_setting')
|
||||
adminRoutes() {
|
||||
let routes = [];
|
||||
|
||||
this.get('model').forEach(p => {
|
||||
if (this.siteSettings[p.get('enabled_setting')] && p.get('admin_route')) {
|
||||
routes.push(p.get('admin_route'));
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}).compact();
|
||||
}.property()
|
||||
});
|
||||
|
|
|
@ -31,6 +31,29 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
}.property('model.user_fields.[]'),
|
||||
|
||||
actions: {
|
||||
|
||||
impersonate() { return this.get("model").impersonate(); },
|
||||
logOut() { return this.get("model").logOut(); },
|
||||
resetBounceScore() { return this.get("model").resetBounceScore(); },
|
||||
refreshBrowsers() { return this.get("model").refreshBrowsers(); },
|
||||
approve() { return this.get("model").approve(); },
|
||||
deactivate() { return this.get("model").deactivate(); },
|
||||
sendActivationEmail() { return this.get("model").sendActivationEmail(); },
|
||||
activate() { return this.get("model").activate(); },
|
||||
revokeAdmin() { return this.get("model").revokeAdmin(); },
|
||||
grantAdmin() { return this.get("model").grantAdmin(); },
|
||||
revokeModeration() { return this.get("model").revokeModeration(); },
|
||||
grantModeration() { return this.get("model").grantModeration(); },
|
||||
saveTrustLevel() { return this.get("model").saveTrustLevel(); },
|
||||
restoreTrustLevel() { return this.get("model").restoreTrustLevel(); },
|
||||
lockTrustLevel(locked) { return this.get("model").lockTrustLevel(locked); },
|
||||
unsuspend() { return this.get("model").unsuspend(); },
|
||||
unblock() { return this.get("model").unblock(); },
|
||||
block() { return this.get("model").block(); },
|
||||
deleteAllPosts() { return this.get("model").deleteAllPosts(); },
|
||||
anonymize() { return this.get('model').anonymize(); },
|
||||
destroy() { return this.get('model').destroy(); },
|
||||
|
||||
toggleTitleEdit() {
|
||||
this.set('userTitleValue', this.get('model.title'));
|
||||
this.toggleProperty('editingTitle');
|
||||
|
@ -39,7 +62,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
saveTitle() {
|
||||
const self = this;
|
||||
|
||||
return ajax("/users/" + this.get('model.username').toLowerCase(), {
|
||||
return ajax(`/users/${this.get('model.username').toLowerCase()}.json`, {
|
||||
data: {title: this.get('userTitleValue')},
|
||||
type: 'PUT'
|
||||
}).catch(function(e) {
|
||||
|
@ -107,14 +130,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
if (result) { self.get('model').revokeApiKey(); }
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
anonymize() {
|
||||
this.get('model').anonymize();
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.get('model').destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,16 @@ export default Ember.Controller.extend({
|
|||
this.set('saved', false);
|
||||
const url = extractDomainFromUrl(this.get('model.payload_url'));
|
||||
const model = this.get('model');
|
||||
const isNew = model.get('isNew');
|
||||
|
||||
const saveWebHook = () => {
|
||||
return model.save().then(() => {
|
||||
this.set('saved', true);
|
||||
this.get('adminWebHooks').get('model').addObject(model);
|
||||
|
||||
if (isNew) {
|
||||
this.transitionToRoute('adminWebHooks.show', model.get('id'));
|
||||
}
|
||||
}).catch(popupAjaxError);
|
||||
};
|
||||
|
||||
|
|
|
@ -257,7 +257,7 @@ const AdminUser = Discourse.User.extend({
|
|||
});
|
||||
},
|
||||
|
||||
log_out() {
|
||||
logOut() {
|
||||
return ajax("/admin/users/" + this.id + "/log_out", {
|
||||
type: 'POST',
|
||||
data: { username_or_email: this.get('username') }
|
||||
|
|
|
@ -5,7 +5,7 @@ EmailPreview.reopenClass({
|
|||
findDigest: function(lastSeenAt, username) {
|
||||
|
||||
if (Em.isEmpty(lastSeenAt)) {
|
||||
lastSeenAt = moment().subtract(7, 'days').format('YYYY-MM-DD');
|
||||
lastSeenAt = this.oneWeekAgo();
|
||||
}
|
||||
|
||||
if (Em.isEmpty(username)) {
|
||||
|
@ -21,7 +21,7 @@ EmailPreview.reopenClass({
|
|||
|
||||
sendDigest: function(lastSeenAt, username, email) {
|
||||
if (Em.isEmpty(lastSeenAt)) {
|
||||
lastSeenAt = moment().subtract(7, 'days').format('YYYY-MM-DD');
|
||||
lastSeenAt = this.oneWeekAgo();
|
||||
}
|
||||
|
||||
if (Em.isEmpty(username)) {
|
||||
|
@ -31,6 +31,11 @@ EmailPreview.reopenClass({
|
|||
return ajax("/admin/email/send-digest.json", {
|
||||
data: { last_seen_at: lastSeenAt, username: username, email: email }
|
||||
});
|
||||
},
|
||||
|
||||
oneWeekAgo() {
|
||||
const en = moment().locale('en');
|
||||
return en.subtract(7, 'days').format('YYYY-MM-DD');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ const Report = Discourse.Model.extend({
|
|||
switch (this.get("type")) {
|
||||
case "flags": return "flag";
|
||||
case "likes": return "heart";
|
||||
case "bookmarks": return "bookmark";
|
||||
default: return null;
|
||||
}
|
||||
}.property("type"),
|
||||
|
|
|
@ -10,7 +10,6 @@ export default RestModel.extend({
|
|||
verify_certificate: true,
|
||||
active: false,
|
||||
web_hook_event_types: null,
|
||||
categoriesFilter: null,
|
||||
groupsFilterInName: null,
|
||||
|
||||
@computed('wildcard_web_hook')
|
||||
|
@ -23,9 +22,9 @@ export default RestModel.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@observes('category_ids')
|
||||
updateCategoriesFilter() {
|
||||
this.set('categoriesFilter', Category.findByIds(this.get('category_ids')));
|
||||
@computed('category_ids')
|
||||
categories(categoryIds) {
|
||||
return Category.findByIds(categoryIds);
|
||||
},
|
||||
|
||||
@observes('group_ids')
|
||||
|
@ -55,7 +54,8 @@ export default RestModel.extend({
|
|||
|
||||
createProperties() {
|
||||
const types = this.get('web_hook_event_types');
|
||||
const categories = this.get('categoriesFilter');
|
||||
const categoryIds = this.get('categories').map(c => c.id);
|
||||
|
||||
// Hack as {{group-selector}} accepts a comma-separated string as data source, but
|
||||
// we use an array to populate the datasource above.
|
||||
const groupsFilter = this.get('groupsFilterInName');
|
||||
|
@ -69,7 +69,7 @@ export default RestModel.extend({
|
|||
verify_certificate: this.get('verify_certificate'),
|
||||
active: this.get('active'),
|
||||
web_hook_event_type_ids: Ember.isEmpty(types) ? [null] : types.map(type => type.id),
|
||||
category_ids: Ember.isEmpty(categories) ? [null] : categories.map(c => c.id),
|
||||
category_ids: Ember.isEmpty(categoryIds) ? [null] : categoryIds,
|
||||
group_ids: Ember.isEmpty(groupNames) || Ember.isEmpty(groupNames[0]) ? [null] : Discourse.Site.currentProp('groups')
|
||||
.reduce((groupIds, g) => {
|
||||
if (groupNames.includes(g.name)) { groupIds.push(g.id); }
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{{nav-item route='admin.backups' label='admin.backups.title'}}
|
||||
{{/if}}
|
||||
{{nav-item route='adminPlugins' label='admin.plugins.title'}}
|
||||
{{plugin-outlet "admin-menu" tagName="li"}}
|
||||
{{plugin-outlet name="admin-menu" connectorTagName="li"}}
|
||||
</ul>
|
||||
|
||||
<div class='boxed white admin-content'>
|
||||
|
|
|
@ -4,8 +4,19 @@
|
|||
{{/if}}
|
||||
<a href="{{report.reportUrl}}">{{report.title}}</a>
|
||||
</td>
|
||||
<td class="value">{{report.todayCount}}</td>
|
||||
<td class="value {{report.yesterdayTrend}}" title={{report.yesterdayCountTitle}}>{{report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
|
||||
<td class="value {{report.sevenDayTrend}}" title={{report.sevenDayCountTitle}}>{{report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
|
||||
<td class="value {{report.thirtyDayTrend}}" title={{report.thirtyDayCountTitle}}>{{report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
|
||||
<td class="value">{{report.total}}</td>
|
||||
|
||||
<td class="value">{{number report.todayCount}}</td>
|
||||
|
||||
<td class="value {{report.yesterdayTrend}}" title={{report.yesterdayCountTitle}}>
|
||||
{{number report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
|
||||
</td>
|
||||
|
||||
<td class="value {{report.sevenDayTrend}}" title={{number report.sevenDayCountTitle}}>
|
||||
{{number report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
|
||||
</td>
|
||||
|
||||
<td class="value {{report.thirtyDayTrend}}" title={{number report.thirtyDayCountTitle}}>
|
||||
{{number report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
|
||||
</td>
|
||||
|
||||
<td class="value">{{number report.total}}</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<td class="title">{{report.title}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'newuser'}}{{value-at-tl report.data level="0"}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'basic'}}{{value-at-tl report.data level="1"}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'member'}}{{value-at-tl report.data level="2"}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'regular'}}{{value-at-tl report.data level="3"}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'leader'}}{{value-at-tl report.data level="4"}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'newuser'}}{{number (value-at-tl report.data level="0")}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'basic'}}{{number (value-at-tl report.data level="1")}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'member'}}{{number (value-at-tl report.data level="2")}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'regular'}}{{number (value-at-tl report.data level="3")}}{{/link-to}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'leader'}}{{number (value-at-tl report.data level="4")}}{{/link-to}}</td>
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
{{text-field value=url disabled=formSubmitted class="permalink-url" placeholderKey="admin.permalink.url" autocorrect="off" autocapitalize="off"}}
|
||||
{{combo-box content=permalinkTypes value=permalinkType}}
|
||||
{{text-field value=permalink_type_value disabled=formSubmitted class="external-url" placeholderKey=permalinkTypePlaceholder autocorrect="off" autocapitalize="off"}}
|
||||
<button class="btn" {{action "submit" target="view"}} disabled={{formSubmitted}}>{{i18n 'admin.permalink.form.add'}}</button>
|
||||
{{d-button action="submit" disabled=formSubmitted label="admin.permalink.form.add"}}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{plugin-outlet "admin-dashboard-top"}}
|
||||
{{plugin-outlet name="admin-dashboard-top"}}
|
||||
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
<div class="dashboard-left">
|
||||
|
@ -228,7 +228,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="value">{{data.num_clicks}}</td>
|
||||
<td class="value">{{number data.num_clicks}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{{/each}}
|
||||
|
@ -248,8 +248,8 @@
|
|||
<tbody>
|
||||
<tr>
|
||||
<td class="title">{{s.domain}}</td>
|
||||
<td class="value">{{s.num_clicks}}</td>
|
||||
<td class="value">{{s.num_topics}}</td>
|
||||
<td class="value">{{number s.num_clicks}}</td>
|
||||
<td class="value">{{number s.num_topics}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{{/each}}
|
||||
|
@ -261,16 +261,16 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th class="title">{{top_referrers.title}} ({{i18n 'admin.dashboard.reports.last_30_days'}})</th>
|
||||
<th>{{top_referrers.ytitles.num_clicks}}</th>
|
||||
<th>{{top_referrers.ytitles.num_topics}}</th>
|
||||
<th>{{number top_referrers.ytitles.num_clicks}}</th>
|
||||
<th>{{number top_referrers.ytitles.num_topics}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{#each top_referrers.data as |r|}}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">{{#link-to 'adminUser' r.user_id r.username}}{{unbound r.username}}{{/link-to}}</td>
|
||||
<td class="value">{{r.num_clicks}}</td>
|
||||
<td class="value">{{r.num_topics}}</td>
|
||||
<td class="value">{{number r.num_clicks}}</td>
|
||||
<td class="value">{{number r.num_topics}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{{/each}}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<tbody>
|
||||
{{#each sortedEmojis as |e|}}
|
||||
<tr>
|
||||
<th><img class="emoji" src="{{unbound e.url}}" title="{{unbound e.name}}"></th>
|
||||
<th><img class="emoji emoji-custom" src="{{unbound e.url}}" title="{{unbound e.name}}"></th>
|
||||
<th>:{{e.name}}:</th>
|
||||
<th><button {{action "destroy" e}} class='btn btn-danger no-text pull-right'>{{fa-icon 'trash-o'}} </button></th>
|
||||
</tr>
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
|
||||
{{#if model.id}}
|
||||
{{#unless model.automatic}}
|
||||
<div>
|
||||
<label for='full_name'>{{i18n 'groups.edit.full_name'}}</label>
|
||||
{{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="bio">{{i18n 'groups.bio'}}</label>
|
||||
{{d-editor value=model.bio_raw}}
|
||||
|
@ -33,26 +38,8 @@
|
|||
</div>
|
||||
{{/unless}}
|
||||
<div>
|
||||
<label>{{i18n 'admin.groups.group_members'}} ({{model.user_count}})</label>
|
||||
<div>
|
||||
<a class="previous {{if showingFirst 'disabled'}}" {{action "previous"}}>{{fa-icon "fast-backward"}}</a>
|
||||
{{currentPage}}/{{totalPages}}
|
||||
<a class="next {{if showingLast 'disabled'}}" {{action "next"}}>{{fa-icon "fast-forward"}}</a>
|
||||
{{group-members-input model=model}}
|
||||
</div>
|
||||
<div class="ac-wrap clearfix">
|
||||
{{#each model.members as |member|}}
|
||||
{{group-member member=member automatic=model.automatic removeAction="removeMember"}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#unless model.automatic}}
|
||||
<div>
|
||||
<label for="user-selector">{{i18n 'admin.groups.add_members'}}</label>
|
||||
{{user-selector usernames=model.usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}}
|
||||
{{d-button action="addMembers" class="add" icon="plus" label="admin.groups.add"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
<div>
|
||||
|
@ -121,7 +108,7 @@
|
|||
{{#if siteSettings.email_in}}
|
||||
<label for="incoming_email">{{i18n 'admin.groups.incoming_email'}}</label>
|
||||
{{text-field name="incoming_email" value=model.incoming_email placeholderKey="admin.groups.incoming_email_placeholder"}}
|
||||
{{plugin-outlet "group-email-in"}}
|
||||
{{plugin-outlet name="group-email-in" args=(hash model=model)}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<li>
|
||||
{{#link-to "adminGroup" group.type group.name}}{{group.name}}
|
||||
{{#if group.userCountDisplay}}
|
||||
<span class="count">{{group.userCountDisplay}}</span>
|
||||
<span class="count">{{number group.userCountDisplay}}</span>
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
|
|
|
@ -9,16 +9,10 @@
|
|||
{{/if}}
|
||||
{{#if model.active}}
|
||||
{{#if model.can_impersonate}}
|
||||
<button class='btn btn-danger' {{action "impersonate" target="content"}} title="{{i18n 'admin.impersonate.help'}}">
|
||||
{{fa-icon "crosshairs"}}
|
||||
{{i18n 'admin.impersonate.title'}}
|
||||
</button>
|
||||
{{d-button class="btn-danger" action="impersonate" icon="crosshairs" label="admin.impersonate.title" title="admin.impersonate.help"}}
|
||||
{{/if}}
|
||||
{{#if currentUser.admin}}
|
||||
<button class='btn' {{action "log_out" target="content"}}>
|
||||
{{fa-icon "power-off"}}
|
||||
{{i18n 'admin.user.log_out'}}
|
||||
</button>
|
||||
{{d-button action="logOut" icon="power-off" label="admin.user.log_out"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
@ -49,7 +43,7 @@
|
|||
{{#if model.email}}
|
||||
<a href="mailto:{{unbound model.email}}">{{model.email}}</a>
|
||||
{{else}}
|
||||
<button class="btn" title="{{i18n 'admin.users.check_email.title'}}" {{action "checkEmail" this}}>{{fa-icon "envelope-o"}} {{i18n 'admin.users.check_email.text'}}</button>
|
||||
{{d-button action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,9 +53,7 @@
|
|||
<div class='value'>{{model.bounceScore}}</div>
|
||||
<div class='controls'>
|
||||
{{#if model.canResetBounceScore}}
|
||||
<button class='btn' {{action "resetBounceScore" target="content"}} title={{i18n "admin.user.reset_bounce_score.title"}}>
|
||||
{{i18n "admin.user.reset_bounce_score.label"}}
|
||||
</button>
|
||||
{{d-button action="resetBounceScore" label="admin.user.reset_bounce_score.label" title="admin.user.reset_bounce_score.title"}}
|
||||
{{/if}}
|
||||
{{model.bounceScoreExplanation}}
|
||||
</div>
|
||||
|
@ -73,7 +65,7 @@
|
|||
{{#if model.associated_accounts}}
|
||||
{{model.associated_accounts}}
|
||||
{{else}}
|
||||
<button class="btn" title="{{i18n 'admin.users.check_email.title'}}" {{action "checkEmail" this}}>{{fa-icon "envelope-o"}} {{i18n 'admin.users.check_email.text'}}</button>
|
||||
{{d-button action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -108,9 +100,7 @@
|
|||
<div class='value'>{{model.ip_address}}</div>
|
||||
<div class='controls'>
|
||||
{{#if currentUser.staff}}
|
||||
<button class='btn' {{action "refreshBrowsers" target="content"}}>
|
||||
{{i18n 'admin.user.refresh_browsers'}}
|
||||
</button>
|
||||
{{d-button action="refreshBrowsers" label="admin.user.refresh_browsers"}}
|
||||
{{ip-lookup ip=model.ip_address userId=model.id}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
@ -176,10 +166,7 @@
|
|||
{{i18n 'admin.user.approve_success'}}
|
||||
{{else}}
|
||||
{{#if model.can_approve}}
|
||||
<button class='btn' {{action "approve" target="content"}}>
|
||||
{{fa-icon "check"}}
|
||||
{{i18n 'admin.user.approve'}}
|
||||
</button>
|
||||
{{d-button action="approve" icon="check" label="admin.user.approve"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
@ -188,31 +175,19 @@
|
|||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.users.active'}}</div>
|
||||
<div class='value'>
|
||||
{{#if model.active}}
|
||||
{{i18n 'yes_value'}}
|
||||
{{else}}
|
||||
{{i18n 'no_value'}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='value'>{{i18n-yes-no model.active}}</div>
|
||||
<div class='controls'>
|
||||
{{#if model.active}}
|
||||
{{#if model.can_deactivate}}
|
||||
<button class='btn' {{action "deactivate" target="content"}}>{{i18n 'admin.user.deactivate_account'}}</button>
|
||||
{{d-button action="deactivate" label="admin.user.deactivate_account"}}
|
||||
{{i18n 'admin.user.deactivate_explanation'}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if model.can_send_activation_email}}
|
||||
<button class='btn' {{action "sendActivationEmail" target="content"}}>
|
||||
{{fa-icon "envelope"}}
|
||||
{{i18n 'admin.user.send_activation_email'}}
|
||||
</button>
|
||||
{{d-button action="sendActivationEmail" icon="envelope" label="admin.user.send_activation_email"}}
|
||||
{{/if}}
|
||||
{{#if model.can_activate}}
|
||||
<button class='btn' {{action "activate" target="content"}}>
|
||||
{{fa-icon "check"}}
|
||||
{{i18n 'admin.user.activate'}}
|
||||
</button>
|
||||
{{d-button action="activate" icon="check" label="admin.user.activate"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
@ -240,38 +215,26 @@
|
|||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.user.admin'}}</div>
|
||||
<div class='value'>{{model.admin}}</div>
|
||||
<div class='value'>{{i18n-yes-no model.admin}}</div>
|
||||
<div class='controls'>
|
||||
{{#if model.can_revoke_admin}}
|
||||
<button class='btn' {{action "revokeAdmin" target="content"}}>
|
||||
{{fa-icon "shield"}}
|
||||
{{i18n 'admin.user.revoke_admin'}}
|
||||
</button>
|
||||
{{d-button action="revokeAdmin" icon="shield" label="admin.user.revoke_admin"}}
|
||||
{{/if}}
|
||||
{{#if model.can_grant_admin}}
|
||||
<button class='btn' {{action "grantAdmin" target="content"}}>
|
||||
{{fa-icon "shield"}}
|
||||
{{i18n 'admin.user.grant_admin'}}
|
||||
</button>
|
||||
{{d-button action="grantAdmin" icon="shield" label="admin.user.grant_admin"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.user.moderator'}}</div>
|
||||
<div class='value'>{{model.moderator}}</div>
|
||||
<div class='value'>{{i18n-yes-no model.moderator}}</div>
|
||||
<div class='controls'>
|
||||
{{#if model.can_revoke_moderation}}
|
||||
<button class='btn' {{action "revokeModeration" target="content"}}>
|
||||
{{fa-icon "shield"}}
|
||||
{{i18n 'admin.user.revoke_moderation'}}
|
||||
</button>
|
||||
{{d-button action="revokeModeration" icon="shield" label="admin.user.revoke_moderation"}}
|
||||
{{/if}}
|
||||
{{#if model.can_grant_moderation}}
|
||||
<button class='btn' {{action "grantModeration" target="content"}}>
|
||||
{{fa-icon "shield"}}
|
||||
{{i18n 'admin.user.grant_moderation'}}
|
||||
</button>
|
||||
{{d-button action="grantModeration" icon="shield" label="admin.user.grant_moderation"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -282,17 +245,17 @@
|
|||
{{combo-box content=site.trustLevels value=model.trust_level nameProperty="detailedName"}}
|
||||
{{#if model.dirty}}
|
||||
<div>
|
||||
<button class='btn ok no-text' {{action "saveTrustLevel" target="content"}}>{{fa-icon "check"}}</button>
|
||||
<button class='btn cancel no-text' {{action "restoreTrustLevel" target="content"}}>{{fa-icon "times"}}</button>
|
||||
{{d-button class="ok no-text" action="saveTrustLevel" icon="check"}}
|
||||
{{d-button class="cancel no-text" action="restoreTrustLevel" icon="times"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="controls">
|
||||
{{#if model.canLockTrustLevel}}
|
||||
{{#if model.trust_level_locked}}
|
||||
<i title='{{i18n 'admin.user.trust_level_locked_tip'}}' {{action "lockTrustLevel" false target="model"}} class='fa fa-lock'></i> <button class="btn" {{action "lockTrustLevel" false target="model"}}>{{i18n 'admin.user.unlock_trust_level'}}</button>
|
||||
<i title='{{i18n 'admin.user.trust_level_locked_tip'}}' class='fa fa-lock'></i> {{d-button action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}}
|
||||
{{else}}
|
||||
<i title='{{i18n 'admin.user.trust_level_unlocked_tip'}}' class='fa fa-unlock'></i> <button class="btn" {{action "lockTrustLevel" true target="model"}}>{{i18n 'admin.user.lock_trust_level'}}</button>
|
||||
<i title='{{i18n 'admin.user.trust_level_unlocked_tip'}}' class='fa fa-unlock'></i> {{d-button action="lockTrustLevel" actionParam=true label="admin.user.lock_trust_level"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if model.tl3Requirements}}
|
||||
|
@ -303,21 +266,15 @@
|
|||
|
||||
<div class="display-row {{if model.isSuspended 'highlight-danger'}}">
|
||||
<div class='field'>{{i18n 'admin.user.suspended'}}</div>
|
||||
<div class='value'>{{model.isSuspended}}</div>
|
||||
<div class='value'>{{i18n-yes-no model.isSuspended}}</div>
|
||||
<div class='controls'>
|
||||
{{#if model.isSuspended}}
|
||||
<button class='btn btn-danger' {{action "unsuspend" target="content"}}>
|
||||
{{fa-icon "ban"}}
|
||||
{{i18n 'admin.user.unsuspend'}}
|
||||
</button>
|
||||
{{d-button class="btn-danger" action="unsuspend" icon="ban" label="admin.user.unsuspend"}}
|
||||
{{suspendDuration}}
|
||||
{{i18n 'admin.user.suspended_explanation'}}
|
||||
{{else}}
|
||||
{{#if model.canSuspend}}
|
||||
<button class='btn btn-danger' {{action "showSuspendModal" this}}>
|
||||
{{fa-icon "ban"}}
|
||||
{{i18n 'admin.user.suspend'}}
|
||||
</button>
|
||||
{{d-button class="btn-danger" action="showSuspendModal" actionParam=model icon="ban" label="admin.user.suspend"}}
|
||||
{{i18n 'admin.user.suspended_explanation'}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
@ -340,20 +297,14 @@
|
|||
|
||||
<div class="display-row {{if model.blocked 'highlight-danger'}}">
|
||||
<div class='field'>{{i18n 'admin.user.blocked'}}</div>
|
||||
<div class='value'>{{model.blocked}}</div>
|
||||
<div class='value'>{{i18n-yes-no model.blocked}}</div>
|
||||
<div class='controls'>
|
||||
{{#conditional-loading-spinner size="small" condition=model.blockingUser}}
|
||||
{{#if model.blocked}}
|
||||
<button class='btn' {{action "unblock" target="content"}}>
|
||||
{{fa-icon "thumbs-o-up"}}
|
||||
{{i18n 'admin.user.unblock'}}
|
||||
</button>
|
||||
{{d-button action="unblock" icon="thumbs-o-up" label="admin.user.unblock"}}
|
||||
{{i18n 'admin.user.block_explanation'}}
|
||||
{{else}}
|
||||
<button class='btn' {{action "block" target="content"}}>
|
||||
{{fa-icon "ban"}}
|
||||
{{i18n 'admin.user.block'}}
|
||||
</button>
|
||||
{{d-button action="block" icon="ban" label="admin.user.block"}}
|
||||
{{i18n 'admin.user.block_explanation'}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
|
@ -362,7 +313,7 @@
|
|||
|
||||
<div class="display-row">
|
||||
<div class='field'>{{i18n 'admin.user.staged'}}</div>
|
||||
<div class='value'>{{model.staged}}</div>
|
||||
<div class='value'>{{i18n-yes-no model.staged}}</div>
|
||||
<div class='controls'>{{i18n 'admin.user.staged_explanation'}}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -423,10 +374,7 @@
|
|||
<div class='controls'>
|
||||
{{#if model.can_delete_all_posts}}
|
||||
{{#if model.post_count}}
|
||||
<button class='btn btn-danger' {{action "deleteAllPosts" target="content"}}>
|
||||
{{fa-icon "trash-o"}}
|
||||
{{i18n 'admin.user.delete_all_posts'}}
|
||||
</button>
|
||||
{{d-button class="btn-danger" action="deleteAllPosts" icon="trash-o" label="admin.user.delete_all_posts"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{model.deleteAllPostsExplanation}}
|
||||
|
|
|
@ -55,8 +55,8 @@
|
|||
<td class='email'>{{unbound user.email}}</td>
|
||||
<td>{{{unbound user.last_emailed_age}}}</td>
|
||||
<td>{{{unbound user.last_seen_age}}}</td>
|
||||
<td>{{{unbound user.topics_entered}}}</td>
|
||||
<td>{{{unbound user.posts_read_count}}}</td>
|
||||
<td>{{number user.topics_entered}}</td>
|
||||
<td>{{number user.posts_read_count}}</td>
|
||||
<td>{{{unbound user.time_read}}}</td>
|
||||
|
||||
<td>{{{unbound user.created_at_age}}}</td>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<div class="dashboard-stats version-check {{if versionCheck.critical_updates 'critical' 'normal'}}">
|
||||
<table class="table table-condensed table-hover">
|
||||
{{custom-html name="upgrade-header" versionCheck=versionCheck tagName="thead"}}
|
||||
</table>
|
||||
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
{{custom-html 'upgrade-header'}}
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{i18n 'admin.dashboard.installed_version'}}</th>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<div class='filters'>
|
||||
<div>
|
||||
<label>{{fa-icon 'circle' class='tracking'}}{{i18n 'admin.web_hooks.categories_filter'}}</label>
|
||||
{{category-selector categories=model.categoriesFilter blacklist=model.categoriesFilter}}
|
||||
{{category-selector categories=model.categories blacklist=model.categories}}
|
||||
<div class="instructions">{{i18n 'admin.web_hooks.categories_filter_instructions'}}</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -89,7 +89,6 @@
|
|||
//= require_tree ./discourse/models
|
||||
//= require_tree ./discourse/components
|
||||
//= require_tree ./discourse/raw-views
|
||||
//= require_tree ./discourse/views
|
||||
//= require_tree ./discourse/helpers
|
||||
//= require_tree ./discourse/templates
|
||||
//= require_tree ./discourse/routes
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
|
||||
registerUnbound('i18n', (key, params) => I18n.t(key, params));
|
||||
registerUnbound('i18n-yes-no', (value, params) => I18n.t(value ? 'yes_value' : 'no_value', params));
|
||||
|
|
|
@ -18,6 +18,10 @@ RawHandlebars.helpers['get'] = function(context, options) {
|
|||
var firstContext = options.contexts[0];
|
||||
var val = firstContext[context];
|
||||
|
||||
if (context.indexOf('controller') === 0) {
|
||||
context = context.replace(/^controller\./, '');
|
||||
}
|
||||
|
||||
if (val && val.isDescriptor) { return Em.get(firstContext, context); }
|
||||
val = val === undefined ? Em.get(firstContext, context): val;
|
||||
return val;
|
||||
|
|
|
@ -10,19 +10,23 @@ export function setResolverOption(name, value) {
|
|||
_options[name] = value;
|
||||
}
|
||||
|
||||
export function getResolverOption(name) {
|
||||
return _options[name];
|
||||
}
|
||||
|
||||
function parseName(fullName) {
|
||||
const nameParts = fullName.split(":"),
|
||||
type = nameParts[0], fullNameWithoutType = nameParts[1],
|
||||
name = fullNameWithoutType,
|
||||
namespace = get(this, 'namespace'),
|
||||
root = namespace;
|
||||
const nameParts = fullName.split(":");
|
||||
const type = nameParts[0];
|
||||
let fullNameWithoutType = nameParts[1];
|
||||
const namespace = get(this, 'namespace');
|
||||
const root = namespace;
|
||||
|
||||
return {
|
||||
fullName: fullName,
|
||||
type: type,
|
||||
fullNameWithoutType: fullNameWithoutType,
|
||||
name: name,
|
||||
root: root,
|
||||
fullName,
|
||||
type,
|
||||
fullNameWithoutType,
|
||||
name: fullNameWithoutType,
|
||||
root,
|
||||
resolveMethodName: "resolve" + classify(type)
|
||||
};
|
||||
}
|
||||
|
@ -125,12 +129,21 @@ export function buildResolver(baseName) {
|
|||
}
|
||||
},
|
||||
|
||||
findConnectorTemplate(parsedName) {
|
||||
const full = parsedName.fullNameWithoutType.replace('components/', '');
|
||||
if (full.indexOf('connectors') === 0) {
|
||||
return Ember.TEMPLATES[`javascripts/${full}`];
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
resolveTemplate(parsedName) {
|
||||
return this.findPluginMobileTemplate(parsedName) ||
|
||||
this.findPluginTemplate(parsedName) ||
|
||||
this.findMobileTemplate(parsedName) ||
|
||||
this.findTemplate(parsedName) ||
|
||||
this.findLoadingTemplate(parsedName) ||
|
||||
this.findConnectorTemplate(parsedName) ||
|
||||
Ember.TEMPLATES.not_found;
|
||||
},
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ const _pluginCallbacks = [];
|
|||
const Discourse = Ember.Application.extend({
|
||||
rootElement: '#main',
|
||||
_docTitle: document.title,
|
||||
__TAGS_INCLUDED__: true,
|
||||
RAW_TEMPLATES: {},
|
||||
|
||||
getURL(url) {
|
||||
if (!url) return url;
|
||||
|
@ -137,7 +137,7 @@ const Discourse = Ember.Application.extend({
|
|||
Discourse.instanceInitializer({
|
||||
name: "_discourse_plugin_" + (++initCount),
|
||||
after: 'inject-objects',
|
||||
initialize: function() {
|
||||
initialize() {
|
||||
withPluginApi(cb.version, cb.code);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,6 +11,10 @@ export default Em.Component.extend(UploadMixin, {
|
|||
return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
|
||||
},
|
||||
|
||||
validateUploadedFilesOptions() {
|
||||
return { imagesOnly: true };
|
||||
},
|
||||
|
||||
uploadDone(upload) {
|
||||
this.setProperties({
|
||||
imageIsNotASquare: upload.width !== upload.height,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { on, observes, default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@computed('placeholderKey')
|
||||
|
@ -18,7 +18,6 @@ export default Ember.Component.extend({
|
|||
var self = this;
|
||||
var selectedBadges;
|
||||
|
||||
var template = getOwner(this).lookup('template:badge-selector-autocomplete.raw');
|
||||
self.$('input').autocomplete({
|
||||
allowAny: false,
|
||||
items: _.isArray(this.get('badgeNames')) ? this.get('badgeNames') : [this.get('badgeNames')],
|
||||
|
@ -43,7 +42,7 @@ export default Ember.Component.extend({
|
|||
});
|
||||
});
|
||||
},
|
||||
template: template
|
||||
template: findRawTemplate('badge-selector-autocomplete')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import Category from 'discourse/models/category';
|
||||
import { on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@observes('categories')
|
||||
|
@ -13,7 +13,6 @@ export default Ember.Component.extend({
|
|||
@on('didInsertElement')
|
||||
_initializeAutocomplete(opts) {
|
||||
const self = this,
|
||||
template = getOwner(this).lookup('template:category-selector-autocomplete.raw'),
|
||||
regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
|
||||
|
||||
this.$('input').autocomplete({
|
||||
|
@ -41,7 +40,7 @@ export default Ember.Component.extend({
|
|||
self.set('categories', categories);
|
||||
});
|
||||
},
|
||||
template,
|
||||
template: findRawTemplate('category-selector-autocomplete'),
|
||||
transformComplete(category) {
|
||||
return categoryBadgeHTML(category, {allowUncategorized: true});
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-
|
|||
import { load } from 'pretty-text/oneboxer';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import InputValidation from 'discourse/models/input-validation';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
import { tinyAvatar,
|
||||
displayErrorForUpload,
|
||||
getUploadMarkdown,
|
||||
|
@ -62,10 +62,9 @@ export default Ember.Component.extend({
|
|||
@on('didInsertElement')
|
||||
_composerEditorInit() {
|
||||
const topicId = this.get('topic.id');
|
||||
const template = getOwner(this).lookup('template:user-selector-autocomplete.raw');
|
||||
const $input = this.$('.d-editor-input');
|
||||
$input.autocomplete({
|
||||
template,
|
||||
template: findRawTemplate('user-selector-autocomplete'),
|
||||
dataSource: term => userSearch({ term, topicId, includeGroups: true }),
|
||||
key: "@",
|
||||
transformComplete: v => v.username || v.name
|
||||
|
@ -168,7 +167,7 @@ export default Ember.Component.extend({
|
|||
post.set('refreshedPost', true);
|
||||
}
|
||||
|
||||
$oneboxes.each((_, o) => load(o, refresh, ajax));
|
||||
$oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id));
|
||||
},
|
||||
|
||||
_warnMentionedGroups($preview) {
|
||||
|
|
|
@ -31,7 +31,7 @@ export default Ember.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@observes('composer.titleLength')
|
||||
@observes('composer.titleLength', 'watchForLink')
|
||||
_titleChanged() {
|
||||
if (this.get('composer.titleLength') === 0) { this.set('autoPosted', false); }
|
||||
if (this.get('autoPosted') || !this.get('watchForLink')) { return; }
|
||||
|
@ -51,15 +51,16 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
_checkForUrl() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) { return; }
|
||||
|
||||
if (this.get('isAbsoluteUrl') && (this.get('composer.reply')||"").length === 0) {
|
||||
// Try to onebox. If success, update post body and title.
|
||||
|
||||
this.set('composer.loading', true);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = this.get('composer.title');
|
||||
|
||||
let loadOnebox = load(link, false, ajax);
|
||||
let loadOnebox = load(link, false, ajax, this.currentUser.id, true);
|
||||
|
||||
if (loadOnebox && loadOnebox.then) {
|
||||
loadOnebox.then( () => {
|
||||
|
@ -82,15 +83,17 @@ export default Ember.Component.extend({
|
|||
this.set('composer.featuredLink', this.get('composer.title'));
|
||||
|
||||
const $h = $(html),
|
||||
header = $h.find('h4').length > 0 ? $h.find('h4') : $h.find('h3');
|
||||
heading = $h.find('h3').length > 0 ? $h.find('h3') : $h.find('h4');
|
||||
|
||||
this.set('composer.reply', this.get('composer.title'));
|
||||
|
||||
if (header.length > 0 && header.text().length > 0) {
|
||||
this.changeTitle(header.text());
|
||||
if (heading.length > 0 && heading.text().length > 0) {
|
||||
this.changeTitle(heading.text());
|
||||
} else {
|
||||
const filename = (this.get('composer.featuredLink')||"").split("/").pop();
|
||||
this.changeTitle(filename);
|
||||
const firstTitle = $h.attr('title') || $h.find("[title]").attr("title");
|
||||
if (firstTitle && firstTitle.length > 0) {
|
||||
this.changeTitle(firstTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -6,6 +6,10 @@ export default Em.Component.extend(UploadMixin, {
|
|||
tagName: "span",
|
||||
uploadUrl: "/invites/upload_csv",
|
||||
|
||||
validateUploadedFilesOptions() {
|
||||
return { csvOnly: true };
|
||||
},
|
||||
|
||||
@computed("uploading")
|
||||
uploadButtonText(uploading) {
|
||||
return uploading ? I18n.t("uploading") : I18n.t("user.invited.bulk_invite.text");
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { getCustomHTML } from 'discourse/helpers/custom-html';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
init() {
|
||||
this._super();
|
||||
const name = this.get('name');
|
||||
const html = getCustomHTML(name);
|
||||
|
||||
if (html) {
|
||||
this.set('html', html);
|
||||
this.set('layoutName', 'components/custom-html-container');
|
||||
} else {
|
||||
const template = getOwner(this).lookup(`template:${name}`);
|
||||
if (template) {
|
||||
this.set('layoutName', name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -11,6 +11,8 @@ import { translations } from 'pretty-text/emoji/data';
|
|||
import { emojiSearch } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
import { getRegister } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
import { determinePostReplaceSelection } from 'discourse/lib/utilities';
|
||||
import deprecated from 'discourse-common/lib/deprecated';
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
|
@ -297,11 +299,10 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
_applyCategoryHashtagAutocomplete() {
|
||||
const template = this.register.lookup('template:category-tag-autocomplete.raw');
|
||||
const siteSettings = this.siteSettings;
|
||||
|
||||
this.$('.d-editor-input').autocomplete({
|
||||
template: template,
|
||||
template: findRawTemplate('category-tag-autocomplete'),
|
||||
key: '#',
|
||||
transformComplete(obj) {
|
||||
if (obj.model) {
|
||||
|
@ -323,11 +324,10 @@ export default Ember.Component.extend({
|
|||
if (!this.siteSettings.enable_emoji) { return; }
|
||||
|
||||
const register = this.register;
|
||||
const template = this.register.lookup('template:emoji-selector-autocomplete.raw');
|
||||
const self = this;
|
||||
|
||||
$editorInput.autocomplete({
|
||||
template: template,
|
||||
template: findRawTemplate('emoji-selector-autocomplete'),
|
||||
key: ":",
|
||||
afterComplete(text) {
|
||||
self.set('value', text);
|
||||
|
@ -526,11 +526,27 @@ export default Ember.Component.extend({
|
|||
|
||||
_replaceText(oldVal, newVal) {
|
||||
const val = this.get('value');
|
||||
const loc = val.indexOf(oldVal);
|
||||
if (loc !== -1) {
|
||||
this.set('value', val.replace(oldVal, newVal));
|
||||
this._selectText(loc + newVal.length, 0);
|
||||
const needleStart = val.indexOf(oldVal);
|
||||
|
||||
if (needleStart === -1) {
|
||||
// Nothing to replace.
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = this.$('textarea.d-editor-input')[0];
|
||||
|
||||
// Determine post-replace selection.
|
||||
const newSelection = determinePostReplaceSelection({
|
||||
selection: { start: textarea.selectionStart, end: textarea.selectionEnd },
|
||||
needle: { start: needleStart, end: needleStart + oldVal.length },
|
||||
replacement: { start: needleStart, end: needleStart + newVal.length }
|
||||
});
|
||||
|
||||
// Replace value (side effect: cursor at the end).
|
||||
this.set('value', val.replace(oldVal, newVal));
|
||||
|
||||
// Restore cursor.
|
||||
this._selectText(newSelection.start, newSelection.end - newSelection.start);
|
||||
},
|
||||
|
||||
_addText(sel, text) {
|
||||
|
@ -553,6 +569,7 @@ export default Ember.Component.extend({
|
|||
applySurround: (head, tail, exampleKey, opts) => this._applySurround(selected, head, tail, exampleKey, opts),
|
||||
applyList: (head, exampleKey) => this._applyList(selected, head, exampleKey),
|
||||
addText: text => this._addText(selected, text),
|
||||
replaceText: text => this._addText({pre: '', post: ''}, text),
|
||||
getText: () => this.get('value'),
|
||||
};
|
||||
|
||||
|
|
|
@ -5,16 +5,10 @@ import { selectedText } from 'discourse/lib/utilities';
|
|||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
function highlight(postNumber) {
|
||||
const $contents = $(`#post_${postNumber} .topic-body`),
|
||||
origColor = $contents.data('orig-color') || $contents.css('backgroundColor');
|
||||
const $contents = $(`#post_${postNumber} .topic-body`);
|
||||
|
||||
$contents.data("orig-color", origColor)
|
||||
.addClass('highlighted')
|
||||
.stop()
|
||||
.animate({ backgroundColor: origColor }, 2500, 'swing', function() {
|
||||
$contents.removeClass('highlighted');
|
||||
$contents.css({'background-color': ''});
|
||||
});
|
||||
$contents.addClass('highlighted');
|
||||
$contents.on('animationend', () => $contents.removeClass('highlighted'));
|
||||
}
|
||||
|
||||
export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
|
||||
|
|
|
@ -11,6 +11,10 @@ export default Em.Component.extend(UploadMixin, {
|
|||
return Ember.isBlank(this.get("name")) ? {} : { name: this.get("name") };
|
||||
}.property("name"),
|
||||
|
||||
validateUploadedFilesOptions() {
|
||||
return { imagesOnly: true };
|
||||
},
|
||||
|
||||
uploadDone(upload) {
|
||||
this.set("name", null);
|
||||
this.sendAction("done", upload);
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { propertyEqual } from 'discourse/lib/computed';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["group-members-input"],
|
||||
|
||||
@computed('model.limit', 'model.offset', 'model.user_count')
|
||||
currentPage(limit, offset, userCount) {
|
||||
if (userCount === 0) { return 0; }
|
||||
|
||||
return Math.floor(offset / limit) + 1;
|
||||
},
|
||||
|
||||
@computed('model.limit', 'model.user_count')
|
||||
totalPages(limit, userCount) {
|
||||
if (userCount === 0) { return 0; }
|
||||
return Math.floor(userCount / limit) + 1;
|
||||
},
|
||||
|
||||
@computed('model.usernames')
|
||||
disableAddButton(usernames) {
|
||||
return !usernames || !(usernames.length > 0);
|
||||
},
|
||||
|
||||
showingFirst: Em.computed.lte("currentPage", 1),
|
||||
showingLast: propertyEqual("currentPage", "totalPages"),
|
||||
|
||||
actions: {
|
||||
next() {
|
||||
if (this.get("showingLast")) { return; }
|
||||
|
||||
const group = this.get("model");
|
||||
const offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count"));
|
||||
group.set("offset", offset);
|
||||
|
||||
return group.findMembers();
|
||||
},
|
||||
|
||||
previous() {
|
||||
if (this.get("showingFirst")) { return; }
|
||||
|
||||
const group = this.get("model");
|
||||
const offset = Math.max(group.get("offset") - group.get("limit"), 0);
|
||||
group.set("offset", offset);
|
||||
|
||||
return group.findMembers();
|
||||
},
|
||||
|
||||
addMembers() {
|
||||
if (Em.isEmpty(this.get("model.usernames"))) { return; }
|
||||
this.get("model").addMembers(this.get("model.usernames")).catch(popupAjaxError);
|
||||
this.set("model.usernames", null);
|
||||
},
|
||||
|
||||
removeMember(member) {
|
||||
const message = I18n.t("groups.edit.delete_member_confirm",{
|
||||
username: member.get("username"),
|
||||
group: this.get("model.name")
|
||||
});
|
||||
|
||||
return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), confirm => {
|
||||
if (confirm) {
|
||||
this.get("model").removeMember(member);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@computed("model.public")
|
||||
canJoinGroup(publicGroup) {
|
||||
return publicGroup;
|
||||
},
|
||||
|
||||
@computed('model.allow_membership_requests', 'model.alias_level')
|
||||
canRequestMembership(allowMembershipRequests, aliasLevel) {
|
||||
return allowMembershipRequests && aliasLevel === 99;
|
||||
},
|
||||
|
||||
@computed("model.is_group_user", "model.id", "groupUserIds")
|
||||
userIsGroupUser(isGroupUser, groupId, groupUserIds) {
|
||||
if (isGroupUser) {
|
||||
return isGroupUser;
|
||||
} else {
|
||||
return !!groupUserIds && groupUserIds.includes(groupId);
|
||||
}
|
||||
},
|
||||
|
||||
@computed
|
||||
joinGroupAction() {
|
||||
return this.currentUser ? 'joinGroup' : 'showLogin';
|
||||
},
|
||||
|
||||
@computed
|
||||
requestMembershipAction() {
|
||||
return this.currentUser ? 'requestMembership' : 'showLogin';
|
||||
},
|
||||
|
||||
actions: {
|
||||
showLogin() {
|
||||
this.sendAction('showLogin');
|
||||
},
|
||||
|
||||
joinGroup() {
|
||||
this.set('updatingMembership', true);
|
||||
const model = this.get('model');
|
||||
|
||||
model.addMembers(this.currentUser.get('username')).then(() => {
|
||||
model.set('is_group_user', true);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('updatingMembership', false);
|
||||
});
|
||||
},
|
||||
|
||||
leaveGroup() {
|
||||
this.set('updatingMembership', true);
|
||||
const model = this.get('model');
|
||||
|
||||
model.removeMember(this.currentUser).then(() => {
|
||||
model.set('is_group_user', false);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('updatingMembership', false);
|
||||
});
|
||||
},
|
||||
|
||||
requestMembership() {
|
||||
const groupName = this.get('model.name');
|
||||
|
||||
Group.loadOwners(groupName).then(result => {
|
||||
const names = result.map(owner => owner.username).join(",");
|
||||
const title = I18n.t('groups.request_membership_pm.title');
|
||||
const body = I18n.t('groups.request_membership_pm.body', { groupName });
|
||||
this.sendAction("createNewMessageViaParams", names, title, body);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { on, observes, default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@computed('placeholderKey')
|
||||
|
@ -19,7 +19,6 @@ export default Ember.Component.extend({
|
|||
var selectedGroups;
|
||||
var groupNames = this.get('groupNames');
|
||||
|
||||
var template = getOwner(this).lookup('template:group-selector-autocomplete.raw');
|
||||
self.$('input').autocomplete({
|
||||
allowAny: false,
|
||||
items: _.isArray(groupNames) ? groupNames : (Ember.isEmpty(groupNames)) ? [] : [groupNames],
|
||||
|
@ -44,7 +43,7 @@ export default Ember.Component.extend({
|
|||
});
|
||||
});
|
||||
},
|
||||
template: template
|
||||
template: findRawTemplate('group-selector-autocomplete')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,6 +10,10 @@ export default Em.Component.extend(UploadMixin, {
|
|||
return `background-image: url(${imageUrl})`.htmlSafe();
|
||||
},
|
||||
|
||||
validateUploadedFilesOptions() {
|
||||
return { imagesOnly: true };
|
||||
},
|
||||
|
||||
uploadDone(upload) {
|
||||
this.set("imageUrl", upload.url);
|
||||
this.set("imageId", upload.id);
|
||||
|
|
|
@ -14,6 +14,7 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
tagName: 'ul',
|
||||
selectedHtml: null,
|
||||
|
||||
classNames: ['mobile-nav'],
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@ export default Ember.Component.extend({
|
|||
this._super();
|
||||
const name = this.get('widget');
|
||||
|
||||
(this.get('delegated') || []).forEach(m => this.set(m, m));
|
||||
|
||||
this.register = getRegister(this);
|
||||
|
||||
this._widgetClass = queryRegistry(name) || this.register.lookupFactory(`widget:${name}`);
|
||||
|
@ -33,7 +31,6 @@ export default Ember.Component.extend({
|
|||
console.error(`Error: Could not find widget: ${name}`);
|
||||
}
|
||||
|
||||
|
||||
this._childEvents = [];
|
||||
this._connected = [];
|
||||
this._dispatched = [];
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
const connector = this.get('connector');
|
||||
this.set('layoutName', connector.templateName);
|
||||
|
||||
const args = this.get('args') || {};
|
||||
Object.keys(args).forEach(key => this.set(key, args[key]));
|
||||
|
||||
const connectorClass = this.get('connector.connectorClass');
|
||||
connectorClass.setupComponent.call(this, args, this);
|
||||
},
|
||||
|
||||
@observes('args')
|
||||
_argsChanged() {
|
||||
const args = this.get('args') || {};
|
||||
Object.keys(args).forEach(key => this.set(key, args[key]));
|
||||
},
|
||||
|
||||
send(name, ...args) {
|
||||
const connectorClass = this.get('connector.connectorClass');
|
||||
const action = connectorClass.actions[name];
|
||||
return action ? action.call(this, ...args) : this._super(name, ...args);
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
A plugin outlet is an extension point for templates where other templates can
|
||||
be inserted by plugins.
|
||||
|
||||
## Usage
|
||||
|
||||
If your handlebars template has:
|
||||
|
||||
```handlebars
|
||||
{{plugin-outlet name="evil-trout"}}
|
||||
```
|
||||
|
||||
Then any handlebars files you create in the `connectors/evil-trout` directory
|
||||
will automatically be appended. For example:
|
||||
|
||||
plugins/hello/assets/javascripts/discourse/templates/connectors/evil-trout/hello.hbs
|
||||
|
||||
With the contents:
|
||||
|
||||
```handlebars
|
||||
<b>Hello World</b>
|
||||
```
|
||||
|
||||
Will insert <b>Hello World</b> at that point in the template.
|
||||
|
||||
## Disabling
|
||||
|
||||
If a plugin returns a disabled status, the outlets will not be wired up for it.
|
||||
The list of disabled plugins is returned via the `Site` singleton.
|
||||
|
||||
**/
|
||||
import { connectorsFor } from 'discourse/lib/plugin-connectors';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'span',
|
||||
connectors: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
const name = this.get('name');
|
||||
|
||||
if (name) {
|
||||
const args = this.get('args');
|
||||
const connectors = connectorsFor(name).filter(con => {
|
||||
return con.connectorClass.shouldRender(args, this);
|
||||
});
|
||||
|
||||
this.set('connectors', connectors);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,4 +1,3 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
|
||||
// we don't want to deselect when we click on buttons that use it
|
||||
|
@ -10,17 +9,22 @@ function willQuote(e) {
|
|||
export default Ember.Component.extend({
|
||||
classNames: ['quote-button'],
|
||||
classNameBindings: ['visible'],
|
||||
|
||||
@computed('quoteState.buffer')
|
||||
visible: buffer => buffer && buffer.length > 0,
|
||||
visible: false,
|
||||
|
||||
_isMouseDown: false,
|
||||
_reselected: false,
|
||||
|
||||
_hideButton() {
|
||||
this.get('quoteState').clear();
|
||||
this.set('visible', false);
|
||||
},
|
||||
|
||||
_selectionChanged() {
|
||||
const quoteState = this.get('quoteState');
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection.isCollapsed) {
|
||||
if (this.get("visible")) this.sendAction("deselectText");
|
||||
if (this.get("visible")) { this._hideButton(); }
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -28,18 +32,22 @@ export default Ember.Component.extend({
|
|||
let firstRange, postId;
|
||||
for (let r = 0; r < selection.rangeCount; r++) {
|
||||
const range = selection.getRangeAt(r);
|
||||
|
||||
if ($(range.endContainer).closest('.cooked').length === 0) return;
|
||||
|
||||
const $ancestor = $(range.commonAncestorContainer);
|
||||
|
||||
firstRange = firstRange || range;
|
||||
postId = postId || $ancestor.closest('.boxed, .reply').data('post-id');
|
||||
|
||||
if ($ancestor.closest(".contents").length === 0 || !postId) {
|
||||
if (this.get("visible")) this.sendAction("deselectText");
|
||||
if (this.get("visible")) { this._hideButton(); }
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.get("quoteState").setProperties({ postId, buffer: selectedText() });
|
||||
quoteState.selected(postId, selectedText());
|
||||
this.set('visible', quoteState.buffer.length > 0);
|
||||
|
||||
// on Desktop, shows the button at the beginning of the selection
|
||||
// on Mobile, shows the button at the end of the selection
|
||||
|
@ -97,11 +105,11 @@ export default Ember.Component.extend({
|
|||
const wait = (isWinphone || isAndroid) ? 250 : 25;
|
||||
const onSelectionChanged = _.debounce(() => this._selectionChanged(), wait);
|
||||
|
||||
$(document).on("mousedown.quote-button", (e) => {
|
||||
$(document).on("mousedown.quote-button", e => {
|
||||
this._isMouseDown = true;
|
||||
this._reselected = false;
|
||||
if (!willQuote(e)) {
|
||||
this.sendAction("deselectText");
|
||||
this._hideButton();
|
||||
}
|
||||
}).on("mouseup.quote-button", () => {
|
||||
this._isMouseDown = false;
|
||||
|
@ -120,7 +128,8 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
click() {
|
||||
this.sendAction("selectText");
|
||||
const { postId, buffer } = this.get('quoteState');
|
||||
this.attrs.selectText(postId, buffer).then(() => this._hideButton());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -48,12 +48,15 @@ export default Em.Component.extend({
|
|||
|
||||
init() {
|
||||
this._super();
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
this._init();
|
||||
this._update();
|
||||
});
|
||||
},
|
||||
|
||||
@observes('searchTerm')
|
||||
_updateOptions() {
|
||||
this._update();
|
||||
Ember.run.debounce(this, this._update, 250);
|
||||
},
|
||||
|
||||
|
|
|
@ -47,6 +47,12 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
|||
this.queueRerender();
|
||||
},
|
||||
|
||||
willRender() {
|
||||
if (this.get('currentUser.staff')) {
|
||||
$('body').addClass('staff');
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
$(window).on('resize.discourse-menu-panel', () => this.afterRender());
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
import renderTag from 'discourse/lib/render-tag';
|
||||
|
||||
function formatTag(t) {
|
||||
return renderTag(t.id, {count: t.count});
|
||||
return renderTag(t.id, {count: t.count, noHref: true});
|
||||
}
|
||||
|
||||
export default Ember.TextField.extend({
|
||||
classNameBindings: [':tag-chooser'],
|
||||
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
|
||||
|
||||
_initValue: function() {
|
||||
init() {
|
||||
this._super();
|
||||
const tags = this.get('tags') || [];
|
||||
this.set('value', tags.join(", "));
|
||||
}.on('init'),
|
||||
|
||||
if (this.get('allowCreate') !== false) {
|
||||
this.set('allowCreate', this.site.get('can_create_tag'));
|
||||
}
|
||||
|
||||
this.set('termMatchesForbidden', false);
|
||||
},
|
||||
|
||||
_valueChanged: function() {
|
||||
const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
|
||||
|
@ -32,18 +39,13 @@ export default Ember.TextField.extend({
|
|||
}
|
||||
}.observes('tags'),
|
||||
|
||||
_initializeTags: function() {
|
||||
const site = this.site,
|
||||
self = this,
|
||||
filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
var limit = this.siteSettings.max_tags_per_topic;
|
||||
const self = this;
|
||||
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||
|
||||
if (this.get('allowCreate') !== false) {
|
||||
this.set('allowCreate', site.get('can_create_tag'));
|
||||
}
|
||||
|
||||
this.set('termMatchesForbidden', false);
|
||||
let limit = this.siteSettings.max_tags_per_topic;
|
||||
|
||||
if (this.get('unlimitedTagCount')) {
|
||||
limit = null;
|
||||
|
@ -77,7 +79,7 @@ export default Ember.TextField.extend({
|
|||
|
||||
callback(data);
|
||||
},
|
||||
createSearchChoice: function(term, data) {
|
||||
createSearchChoice(term, data) {
|
||||
term = term.replace(filterRegexp, '').trim().toLowerCase();
|
||||
|
||||
// No empty terms, make sure the user has permission to create the tag
|
||||
|
@ -89,14 +91,14 @@ export default Ember.TextField.extend({
|
|||
return { id: term, text: term };
|
||||
}
|
||||
},
|
||||
createSearchChoicePosition: function(list, item) {
|
||||
createSearchChoicePosition(list, item) {
|
||||
// Search term goes on the bottom
|
||||
list.push(item);
|
||||
},
|
||||
formatSelection: function (data) {
|
||||
return data ? renderTag(this.text(data)) : undefined;
|
||||
formatSelection(data) {
|
||||
return data ? renderTag(this.text(data), {noHref: true}) : undefined;
|
||||
},
|
||||
formatSelectionCssClass: function(){
|
||||
formatSelectionCssClass() {
|
||||
return "discourse-tag-select2";
|
||||
},
|
||||
formatResult: formatTag,
|
||||
|
@ -127,10 +129,11 @@ export default Ember.TextField.extend({
|
|||
}
|
||||
},
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
},
|
||||
|
||||
_destroyTags: function() {
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
this.$().select2('destroy');
|
||||
}.on('willDestroyElement')
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import DelegatedActions from 'discourse/mixins/delegated-actions';
|
||||
|
||||
export default Ember.Component.extend(DelegatedActions, {
|
||||
export default Ember.Component.extend({
|
||||
elementId: 'topic-footer-buttons',
|
||||
|
||||
// Allow us to extend it
|
||||
layoutName: 'components/topic-footer-buttons',
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.delegateAll(this.get('topicDelegated'));
|
||||
},
|
||||
|
||||
@computed('topic.details.can_invite_to')
|
||||
canInviteTo(result) {
|
||||
return !this.site.mobileView && result;
|
||||
|
|
|
@ -38,7 +38,6 @@ export default Combobox.extend({
|
|||
@observes('value')
|
||||
_valueChanged() {
|
||||
const value = this.get('value');
|
||||
const controller = this.get('parentView.controller');
|
||||
const topic = this.get('topic');
|
||||
|
||||
const refresh = () => {
|
||||
|
@ -48,7 +47,7 @@ export default Combobox.extend({
|
|||
|
||||
switch(value) {
|
||||
case 'invite':
|
||||
controller.send('showInvite');
|
||||
this.attrs.showInvite();
|
||||
refresh();
|
||||
break;
|
||||
case 'bookmark':
|
||||
|
@ -59,7 +58,7 @@ export default Combobox.extend({
|
|||
refresh();
|
||||
break;
|
||||
case 'flag':
|
||||
controller.send('showFlagTopic');
|
||||
this.attrs.showFlagTopic();
|
||||
refresh();
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { bufferedRender } from 'discourse-common/lib/buffered-render';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
export function showEntrance(e) {
|
||||
let target = $(e.target);
|
||||
|
@ -32,7 +32,7 @@ export default Ember.Component.extend(bufferedRender({
|
|||
},
|
||||
|
||||
buildBuffer(buffer) {
|
||||
const template = getOwner(this).lookup('template:list/topic-list-item.raw');
|
||||
const template = findRawTemplate('list/topic-list-item');
|
||||
if (template) {
|
||||
buffer.push(template(this));
|
||||
}
|
||||
|
@ -128,14 +128,11 @@ export default Ember.Component.extend(bufferedRender({
|
|||
|
||||
highlight(opts = { isLastViewedTopic: false }) {
|
||||
const $topic = this.$();
|
||||
const originalCol = $topic.css('backgroundColor');
|
||||
$topic
|
||||
.addClass('highlighted')
|
||||
.attr('data-islastviewedtopic', opts.isLastViewedTopic)
|
||||
.stop()
|
||||
.animate({ backgroundColor: originalCol }, 2500, 'swing', function() {
|
||||
$topic.removeClass('highlighted');
|
||||
});
|
||||
.attr('data-islastviewedtopic', opts.isLastViewedTopic);
|
||||
|
||||
$topic.on('animationend', () => $topic.removeClass('highlighted'));
|
||||
},
|
||||
|
||||
_highlightIfNeeded: function() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
composerOpen: null,
|
||||
|
@ -94,18 +95,12 @@ export default Ember.Component.extend({
|
|||
|
||||
keyboardTrigger(e) {
|
||||
if (e.type === "jump") {
|
||||
bootbox.prompt(I18n.t('topic.progress.jump_prompt_long'), postIndex => {
|
||||
if (postIndex === null) { return; }
|
||||
this.sendAction('jumpToIndex', postIndex);
|
||||
const controller = showModal('jump-to-post');
|
||||
controller.setProperties({
|
||||
topic: this.get('topic'),
|
||||
postNumber: 1,
|
||||
jumpToIndex: this.attrs.jumpToIndex
|
||||
});
|
||||
|
||||
// this is insanely hacky, for some reason shown event never fires,
|
||||
// something is bust in bootbox
|
||||
// TODO upgrade bootbox to see if this hack can be removed
|
||||
setTimeout(()=>{
|
||||
$('.bootbox.modal').trigger('shown');
|
||||
},50);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -3,17 +3,11 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor
|
|||
export default Ember.Component.extend({
|
||||
elementId: 'topic-progress-wrapper',
|
||||
classNameBindings: ['docked'],
|
||||
expanded: false,
|
||||
docked: false,
|
||||
progressPosition: null,
|
||||
postStream: Ember.computed.alias('topic.postStream'),
|
||||
_streamPercentage: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
(this.get('delegated') || []).forEach(m => this.set(m, m));
|
||||
},
|
||||
|
||||
@computed('progressPosition')
|
||||
jumpTopDisabled(progressPosition) {
|
||||
return progressPosition <= 3;
|
||||
|
@ -43,6 +37,15 @@ export default Ember.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@computed('progressPosition', 'topic.last_read_post_id')
|
||||
showBackButton(position, lastReadId) {
|
||||
if (!lastReadId) { return; }
|
||||
|
||||
const stream = this.get('postStream.stream');
|
||||
const readPos = stream.indexOf(lastReadId) || 0;
|
||||
return (readPos < (stream.length - 1)) && (readPos > position);
|
||||
},
|
||||
|
||||
@observes('postStream.stream.[]')
|
||||
_updateBar() {
|
||||
Ember.run.scheduleOnce('afterRender', this, this._updateProgressBar);
|
||||
|
@ -146,6 +149,10 @@ export default Ember.Component.extend({
|
|||
actions: {
|
||||
toggleExpansion() {
|
||||
this.toggleProperty('expanded');
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.attrs.jumpToPost(this.get('topic.last_read_post_number'));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ const clickMention = "click.discourse-user-mention";
|
|||
|
||||
export default Ember.Component.extend(CleansUp, {
|
||||
elementId: 'user-card',
|
||||
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage'],
|
||||
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'],
|
||||
allowBackgrounds: setting('allow_profile_backgrounds'),
|
||||
|
||||
postStream: Ember.computed.alias('topic.postStream'),
|
||||
|
@ -71,18 +71,14 @@ export default Ember.Component.extend(CleansUp, {
|
|||
|
||||
@observes('user.card_background')
|
||||
addBackground() {
|
||||
const url = this.get('user.card_background');
|
||||
|
||||
if (!this.get('allowBackgrounds')) { return; }
|
||||
|
||||
const $this = this.$();
|
||||
if (!$this) { return; }
|
||||
|
||||
if (Ember.isEmpty(url)) {
|
||||
$this.css('background-image', '').addClass('no-bg');
|
||||
} else {
|
||||
$this.css('background-image', `url(${Discourse.getURLWithCDN(url)})`).removeClass('no-bg');
|
||||
}
|
||||
const url = this.get('user.card_background');
|
||||
const bg = Ember.isEmpty(url) ? '' : `url(${Discourse.getURLWithCDN(url)})`;
|
||||
$this.css('background-image', bg);
|
||||
},
|
||||
|
||||
_show(username, $target) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
import TextField from 'discourse/components/text-field';
|
||||
import userSearch from 'discourse/lib/user-search';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
export default TextField.extend({
|
||||
@observes('usernames')
|
||||
|
@ -31,7 +31,7 @@ export default TextField.extend({
|
|||
}
|
||||
|
||||
this.$().val(this.get('usernames')).autocomplete({
|
||||
template: getOwner(this).lookup('template:user-selector-autocomplete.raw'),
|
||||
template: findRawTemplate('user-selector-autocomplete'),
|
||||
disabled: this.get('disabled'),
|
||||
single: this.get('single'),
|
||||
allowAny: this.get('allowAny'),
|
||||
|
|
|
@ -15,4 +15,10 @@ export default Ember.Controller.extend({
|
|||
loginRequired() {
|
||||
return Discourse.SiteSettings.login_required && !Discourse.User.current();
|
||||
},
|
||||
|
||||
actions: {
|
||||
appRouteAction(name) {
|
||||
return this.send(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
import { allowsImages } from 'discourse/lib/utilities';
|
||||
|
||||
|
@ -34,8 +35,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
refreshGravatar() {
|
||||
this.set("gravatarRefreshDisabled", true);
|
||||
return Discourse
|
||||
.ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
|
||||
return ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
|
||||
.then(result => this.setProperties({
|
||||
gravatar_avatar_template: result.gravatar_avatar_template,
|
||||
gravatar_avatar_upload_id: result.gravatar_upload_id,
|
||||
|
|
|
@ -404,7 +404,6 @@ export default Ember.Controller.extend({
|
|||
|
||||
save(force) {
|
||||
const composer = this.get('model');
|
||||
const self = this;
|
||||
|
||||
// Clear the warning state if we're not showing the checkbox anymore
|
||||
if (!this.get('showWarning')) {
|
||||
|
@ -437,10 +436,10 @@ export default Ember.Controller.extend({
|
|||
buttons.push({
|
||||
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + currentTopic.get('fancyTitle') + "</div>",
|
||||
"class": "btn btn-reply-here",
|
||||
"callback": function() {
|
||||
callback: () => {
|
||||
composer.set('topic', currentTopic);
|
||||
composer.set('post', null);
|
||||
self.save(true);
|
||||
this.save(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -448,9 +447,7 @@ export default Ember.Controller.extend({
|
|||
buttons.push({
|
||||
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + this.get('model.topic.fancyTitle') + "</div>",
|
||||
"class": "btn-primary btn-reply-on-original",
|
||||
"callback": function() {
|
||||
self.save(true);
|
||||
}
|
||||
callback: () => this.save(true)
|
||||
});
|
||||
|
||||
bootbox.dialog(message, buttons, { "classes": "reply-where-modal" });
|
||||
|
@ -471,29 +468,32 @@ export default Ember.Controller.extend({
|
|||
}
|
||||
});
|
||||
|
||||
const promise = composer.save({ imageSizes, editReason: this.get("editReason")}).then(function(result) {
|
||||
const promise = composer.save({ imageSizes, editReason: this.get("editReason")}).then(result=> {
|
||||
if (result.responseJson.action === "enqueued") {
|
||||
self.send('postWasEnqueued', result.responseJson);
|
||||
self.destroyDraft();
|
||||
self.close();
|
||||
self.appEvents.trigger('post-stream:refresh');
|
||||
this.send('postWasEnqueued', result.responseJson);
|
||||
this.destroyDraft();
|
||||
this.close();
|
||||
this.appEvents.trigger('post-stream:refresh');
|
||||
return result;
|
||||
}
|
||||
|
||||
// If user "created a new topic/post" or "replied as a new topic" successfully, remove the draft.
|
||||
if (result.responseJson.action === "create_post" || self.get('replyAsNewTopicDraft')) {
|
||||
self.destroyDraft();
|
||||
if (result.responseJson.action === "create_post" || this.get('replyAsNewTopicDraft')) {
|
||||
this.destroyDraft();
|
||||
}
|
||||
if (this.get('model.action') === 'edit') {
|
||||
this.appEvents.trigger('post-stream:refresh', { id: parseInt(result.responseJson.id) });
|
||||
if (result.responseJson.post.post_number === 1) {
|
||||
this.appEvents.trigger('header:show-topic', composer.get('topic'));
|
||||
}
|
||||
if (self.get('model.action') === 'edit') {
|
||||
self.appEvents.trigger('post-stream:refresh', { id: parseInt(result.responseJson.id) });
|
||||
} else {
|
||||
self.appEvents.trigger('post-stream:refresh');
|
||||
this.appEvents.trigger('post-stream:refresh');
|
||||
}
|
||||
|
||||
if (result.responseJson.action === "create_post") {
|
||||
self.appEvents.trigger('post:highlight', result.payload.post_number);
|
||||
this.appEvents.trigger('post:highlight', result.payload.post_number);
|
||||
}
|
||||
self.close();
|
||||
this.close();
|
||||
|
||||
const currentUser = Discourse.User.current();
|
||||
if (composer.get('creatingTopic')) {
|
||||
|
@ -510,9 +510,9 @@ export default Ember.Controller.extend({
|
|||
}
|
||||
}
|
||||
|
||||
}).catch(function(error) {
|
||||
}).catch(error => {
|
||||
composer.set('disableDrafts', false);
|
||||
self.appEvents.one('composer:will-open', () => bootbox.alert(error));
|
||||
this.appEvents.one('composer:will-open', () => bootbox.alert(error));
|
||||
});
|
||||
|
||||
if (this.get('application.currentRouteName').split('.')[0] === 'topic' &&
|
||||
|
|
|
@ -114,7 +114,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
email = this.get("accountEmail");
|
||||
|
||||
if (this.get('rejectedEmails').contains(email)) {
|
||||
if (this.get('rejectedEmails').includes(email)) {
|
||||
return InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.email.invalid')
|
||||
|
@ -314,7 +314,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.get('rejectedPasswords').contains(password)) {
|
||||
if (this.get('rejectedPasswords').includes(password)) {
|
||||
return InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.password.common')
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
@computed('model.is_group_user')
|
||||
showGroupMessages(isGroupUser) {
|
||||
return isGroupUser || (this.currentUser && this.currentUser.admin);
|
||||
}
|
||||
});
|
|
@ -1,16 +1,19 @@
|
|||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
saving: false,
|
||||
@computed('saving')
|
||||
savingText(saving) {
|
||||
if (saving !== undefined) {
|
||||
return saving ? I18n.t('saving') : I18n.t('saved');
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saving', true);
|
||||
|
||||
this.get('model').save().then(() => {
|
||||
this.transitionToRoute('group', this.get('model.name'));
|
||||
this.send('closeModal');
|
||||
}).catch(error => {
|
||||
this.get('model').save().catch(error => {
|
||||
popupAjaxError(error);
|
||||
}).finally(() => {
|
||||
this.set('saving', false);
|
|
@ -10,24 +10,28 @@ export default Ember.Controller.extend({
|
|||
limit: null,
|
||||
offset: null,
|
||||
isOwner: Ember.computed.alias('model.is_group_owner'),
|
||||
showActions: false,
|
||||
|
||||
@observes('order', 'desc')
|
||||
refreshMembers() {
|
||||
this.set('loading', true);
|
||||
|
||||
this.get('model') &&
|
||||
this.get('model').findMembers({ order: this.get('order'), desc: this.get('desc') });
|
||||
this.get('model')
|
||||
.findMembers({ order: this.get('order'), desc: this.get('desc') })
|
||||
.finally(() => this.set('loading', false));
|
||||
},
|
||||
|
||||
@computed("model.public")
|
||||
canJoinGroup(publicGroup) {
|
||||
return !!(this.currentUser) && publicGroup;
|
||||
},
|
||||
|
||||
@computed('model.allow_membership_requests', 'model.alias_level')
|
||||
canRequestMembership(allowMembershipRequests, aliasLevel) {
|
||||
return !!(this.currentUser) && allowMembershipRequests && aliasLevel === 99;
|
||||
@computed('model.members')
|
||||
hasMembers(members) {
|
||||
return members && members.length > 0;
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleActions() {
|
||||
this.toggleProperty("showActions");
|
||||
},
|
||||
|
||||
removeMember(user) {
|
||||
this.get('model').removeMember(user);
|
||||
},
|
||||
|
@ -39,35 +43,6 @@ export default Ember.Controller.extend({
|
|||
}
|
||||
},
|
||||
|
||||
requestMembership() {
|
||||
const groupName = this.get('model.name');
|
||||
const title = I18n.t('groups.request_membership_pm.title');
|
||||
const body = I18n.t('groups.request_membership_pm.body', { groupName });
|
||||
this.transitionToRoute(`/new-message?groupname=${groupName}&title=${title}&body=${body}`);
|
||||
},
|
||||
|
||||
joinGroup() {
|
||||
this.set('updatingMembership', true);
|
||||
const model = this.get('model');
|
||||
|
||||
model.addMembers(this.currentUser.get('username')).then(() => {
|
||||
model.set('is_group_user', true);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('updatingMembership', false);
|
||||
});
|
||||
},
|
||||
|
||||
leaveGroup() {
|
||||
this.set('updatingMembership', true);
|
||||
const model = this.get('model');
|
||||
|
||||
model.removeMember(this.currentUser).then(() => {
|
||||
model.set('is_group_user', false);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('updatingMembership', false);
|
||||
});
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (this.get("loading")) { return; }
|
||||
if (this.get("model.members.length") >= this.get("model.user_count")) { return; }
|
||||
|
|
|
@ -13,17 +13,18 @@ var Tab = Em.Object.extend({
|
|||
});
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
counts: null,
|
||||
showing: 'members',
|
||||
|
||||
tabs: [
|
||||
Tab.create({ name: 'members', active: true, 'location': 'group.index' }),
|
||||
Tab.create({ name: 'posts' }),
|
||||
Tab.create({ name: 'topics' }),
|
||||
Tab.create({ name: 'mentions' }),
|
||||
Tab.create({ name: 'messages', requiresMembership: true }),
|
||||
Tab.create({ name: 'members', 'location': 'group.index', icon: 'users' }),
|
||||
Tab.create({ name: 'activity' }),
|
||||
Tab.create({
|
||||
name: 'logs', i18nKey: 'logs.title', icon: 'shield',
|
||||
requiresMembership: true, requiresGroupAdmin: true
|
||||
name: 'edit', i18nKey: 'edit.title', icon: 'pencil', requiresGroupAdmin: true
|
||||
}),
|
||||
Tab.create({
|
||||
name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', requiresGroupAdmin: true
|
||||
})
|
||||
],
|
||||
|
||||
|
@ -32,9 +33,9 @@ export default Ember.Controller.extend({
|
|||
return !automatic && isGroupOwner;
|
||||
},
|
||||
|
||||
@computed('model.name', 'model.title')
|
||||
groupName(name, title) {
|
||||
return (title || name).capitalize();
|
||||
@computed('model.name', 'model.full_name')
|
||||
groupName(name, fullName) {
|
||||
return (fullName || name).capitalize();
|
||||
},
|
||||
|
||||
@computed('model.name', 'model.flair_url', 'model.flair_bg_color', 'model.flair_color')
|
||||
|
@ -52,31 +53,18 @@ export default Ember.Controller.extend({
|
|||
this.get('tabs')[0].set('count', this.get('model.user_count'));
|
||||
},
|
||||
|
||||
@observes('showing')
|
||||
showingChanged() {
|
||||
const showing = this.get('showing');
|
||||
|
||||
this.get('tabs').forEach(tab => {
|
||||
tab.set('active', showing === tab.get('name'));
|
||||
});
|
||||
},
|
||||
|
||||
@computed('model.is_group_user', 'model.is_group_owner')
|
||||
getTabs(isGroupUser, isGroupOwner) {
|
||||
@computed('model.is_group_user', 'model.is_group_owner', 'model.automatic')
|
||||
getTabs(isGroupUser, isGroupOwner, automatic) {
|
||||
return this.get('tabs').filter(t => {
|
||||
let isMember = false;
|
||||
let display = true;
|
||||
|
||||
if (this.currentUser) {
|
||||
let admin = this.currentUser.admin;
|
||||
|
||||
if (t.get('requiresGroupAdmin')) {
|
||||
isMember = admin || isGroupOwner;
|
||||
} else {
|
||||
isMember = admin || isGroupUser;
|
||||
}
|
||||
if (this.currentUser && t.get('requiresGroupAdmin')) {
|
||||
display = automatic ? false : (this.currentUser.admin || isGroupOwner);
|
||||
} else if (t.get('requiresGroupAdmin')) {
|
||||
display = false;
|
||||
}
|
||||
|
||||
return isMember || !t.get('requiresMembership');
|
||||
return display;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
@observes("groups.canLoadMore")
|
||||
_showFooter() {
|
||||
this.set("application.showFooter", !this.get("groups.canLoadMore"));
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
this.get('groups').loadMore();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -109,6 +109,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
return !prevHidden && this.currentUser && this.currentUser.get('staff');
|
||||
},
|
||||
|
||||
@computed("model.wiki", "model.last_revision", "model.current_revision")
|
||||
displayEdit(wiki, lastRevision, currentRevision) {
|
||||
return wiki && (lastRevision === currentRevision);
|
||||
},
|
||||
|
||||
@computed()
|
||||
displayRevert() {
|
||||
return this.currentUser && this.currentUser.get('staff');
|
||||
|
@ -187,6 +192,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
hideVersion() { this.hide(this.get("model.post_id"), this.get("model.current_revision")); },
|
||||
showVersion() { this.show(this.get("model.post_id"), this.get("model.current_revision")); },
|
||||
|
||||
editWiki() {
|
||||
this.get('topicController').send('editPost', this.get('post'));
|
||||
this.send('closeModal');
|
||||
},
|
||||
|
||||
revertToVersion() { this.revert(this.get("post"), this.get("model.current_revision")); },
|
||||
|
||||
displayInline() { this.set("viewMode", "inline"); },
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
model: null,
|
||||
postNumber: null,
|
||||
|
||||
actions: {
|
||||
jump() {
|
||||
let where = parseInt(this.get('postNumber'));
|
||||
if (where < 1) { where = 1; }
|
||||
const max = this.get('topic.postStream.filteredPostsCount');
|
||||
if (where > max) { where = max; }
|
||||
|
||||
this.jumpToIndex(where);
|
||||
this.send('closeModal');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,4 +1,7 @@
|
|||
import { propertyEqual } from 'discourse/lib/computed';
|
||||
import InputValidation from 'discourse/models/input-validation';
|
||||
import { emailValid } from 'discourse/lib/utilities';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
taken: false,
|
||||
|
@ -8,7 +11,7 @@ export default Ember.Controller.extend({
|
|||
newEmail: null,
|
||||
|
||||
newEmailEmpty: Em.computed.empty('newEmail'),
|
||||
saveDisabled: Em.computed.or('saving', 'newEmailEmpty', 'taken', 'unchanged'),
|
||||
saveDisabled: Em.computed.or('saving', 'newEmailEmpty', 'taken', 'unchanged', 'invalidEmail'),
|
||||
unchanged: propertyEqual('newEmailLower', 'currentUser.email'),
|
||||
|
||||
newEmailLower: function() {
|
||||
|
@ -20,6 +23,21 @@ export default Ember.Controller.extend({
|
|||
return I18n.t("user.change");
|
||||
}.property('saving'),
|
||||
|
||||
@computed('newEmail')
|
||||
invalidEmail(newEmail) {
|
||||
return !emailValid(newEmail);
|
||||
},
|
||||
|
||||
@computed('invalidEmail')
|
||||
emailValidation(invalidEmail) {
|
||||
if (invalidEmail) {
|
||||
return InputValidation.create({
|
||||
failed: true,
|
||||
reason: I18n.t('user.email.invalid')
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
changeEmail: function() {
|
||||
var self = this;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import TagGroup from 'discourse/models/tag-group';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
actions: {
|
||||
selectTagGroup(tagGroup) {
|
||||
|
@ -9,8 +11,7 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
newTagGroup() {
|
||||
const newTagGroup = this.store.createRecord('tag-group');
|
||||
newTagGroup.set('name', I18n.t('tagging.groups.new_name'));
|
||||
const newTagGroup = TagGroup.create({ id: 'new', name: I18n.t('tagging.groups.new_name') });
|
||||
this.get('model').pushObject(newTagGroup);
|
||||
this.send('selectTagGroup', newTagGroup);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ if (customNavItemHref) {
|
|||
if (navItem.get('tagId')) {
|
||||
var name = navItem.get('name');
|
||||
|
||||
if ( !Discourse.Site.currentProp('filters').contains(name) ) {
|
||||
if ( !Discourse.Site.currentProp('filters').includes(name) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
|||
import Post from 'discourse/models/post';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
||||
import QuoteState from 'discourse/lib/quote-state';
|
||||
|
||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
composer: Ember.inject.controller(),
|
||||
|
@ -32,30 +33,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
filter: null,
|
||||
quoteState: null,
|
||||
|
||||
topicDelegated: [
|
||||
'toggleMultiSelect',
|
||||
'deleteTopic',
|
||||
'recoverTopic',
|
||||
'toggleClosed',
|
||||
'showAutoClose',
|
||||
'showFeatureTopic',
|
||||
'showChangeTimestamp',
|
||||
'toggleArchived',
|
||||
'toggleVisibility',
|
||||
'convertToPublicTopic',
|
||||
'convertToPrivateMessage',
|
||||
'jumpTop',
|
||||
'jumpToPost',
|
||||
'jumpToPostPrompt',
|
||||
'jumpToIndex',
|
||||
'jumpBottom',
|
||||
'replyToPost',
|
||||
'toggleArchiveMessage',
|
||||
'showInvite',
|
||||
'toggleBookmark',
|
||||
'showFlagTopic'
|
||||
],
|
||||
|
||||
updateQueryParams() {
|
||||
const postStream = this.get('model.postStream');
|
||||
this.setProperties(postStream.get('streamFilters'));
|
||||
|
@ -143,7 +120,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
this._super();
|
||||
this.set('selectedPosts', []);
|
||||
this.set('selectedReplies', []);
|
||||
this.set('quoteState', Ember.Object.create({ buffer: null, postId: null }));
|
||||
this.set('quoteState', new QuoteState());
|
||||
},
|
||||
|
||||
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
|
||||
|
@ -175,18 +152,29 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
|
||||
actions: {
|
||||
|
||||
deselectText() {
|
||||
this.get('quoteState').setProperties({ buffer: null, postId: null });
|
||||
showPostFlags(post) {
|
||||
return this.send('showFlags', post);
|
||||
},
|
||||
|
||||
selectText() {
|
||||
const { postId, buffer } = this.get('quoteState');
|
||||
topicRouteAction(name, model) {
|
||||
return this.send(name, model);
|
||||
},
|
||||
|
||||
this.send('deselectText');
|
||||
openAutoClose() {
|
||||
this.send('showAutoClose');
|
||||
},
|
||||
|
||||
openFeatureTopic() {
|
||||
this.send('showFeatureTopic');
|
||||
},
|
||||
|
||||
selectText(postId, buffer) {
|
||||
return this.get('model.postStream').loadPost(postId).then(post => {
|
||||
const composer = this.get('composer');
|
||||
const viewOpen = composer.get('model.viewOpen');
|
||||
|
||||
this.get('model.postStream').loadPost(postId).then(post => {
|
||||
// If we can't create a post, delegate to reply as new topic
|
||||
if (!this.get('model.details.can_create_post')) {
|
||||
if ((!viewOpen) && (!this.get('model.details.can_create_post'))) {
|
||||
this.send('replyAsNewTopic', post);
|
||||
return;
|
||||
}
|
||||
|
@ -203,16 +191,19 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
}
|
||||
|
||||
// If the composer is associated with a different post, we don't change it.
|
||||
const composer = this.get('composer');
|
||||
const composerPost = composer.get('content.post');
|
||||
const composerPost = composer.get('model.post');
|
||||
if (composerPost && (composerPost.get('id') !== this.get('post.id'))) {
|
||||
composerOpts.post = composerPost;
|
||||
}
|
||||
|
||||
const quotedText = Quote.build(post, buffer);
|
||||
composerOpts.quote = quotedText;
|
||||
if (composer.get('content.viewOpen') || composer.get('content.viewDraft')) {
|
||||
if (composer.get('model.viewOpen')) {
|
||||
this.appEvents.trigger('composer:insert-text', quotedText);
|
||||
} else if (composer.get('model.viewDraft')) {
|
||||
const model = composer.get('model');
|
||||
model.set('reply', model.get('reply') + quotedText);
|
||||
composer.send('openIfDraft');
|
||||
} else {
|
||||
composer.open(composerOpts);
|
||||
}
|
||||
|
@ -322,9 +313,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
|
||||
const quoteState = this.get('quoteState');
|
||||
const postStream = this.get('model.postStream');
|
||||
const quotedPost = postStream.findLoadedPost(quoteState.get('postId'));
|
||||
const quotedText = Quote.build(quotedPost, quoteState.get('buffer'));
|
||||
this.send('deselectText');
|
||||
const quotedPost = postStream.findLoadedPost(quoteState.postId);
|
||||
const quotedText = Quote.build(quotedPost, quoteState.buffer);
|
||||
|
||||
quoteState.clear();
|
||||
|
||||
if (composerController.get('content.topic.id') === topic.get('id') &&
|
||||
composerController.get('content.action') === Composer.REPLY) {
|
||||
|
@ -462,7 +454,15 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
},
|
||||
|
||||
jumpToPost(postNumber) {
|
||||
this._jumpToPostId(this.get('model.postStream').findPostIdForPostNumber(postNumber));
|
||||
const postStream = this.get('model.postStream');
|
||||
let postId = postStream.findPostIdForPostNumber(postNumber);
|
||||
|
||||
// If we couldn't find the post, find the closest post to it
|
||||
if (!postId) {
|
||||
const closest = postStream.closestPostNumberFor(postNumber);
|
||||
postId = postStream.findPostIdForPostNumber(closest);
|
||||
}
|
||||
this._jumpToPostId(postId);
|
||||
},
|
||||
|
||||
jumpTop() {
|
||||
|
@ -652,10 +652,9 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
replyAsNewTopic(post) {
|
||||
const composerController = this.get('composer');
|
||||
|
||||
const quoteState = this.get('quoteState');
|
||||
post = post || quoteState.get('post');
|
||||
const quotedText = Quote.build(post, quoteState.get('buffer'));
|
||||
this.send('deselectText');
|
||||
const { quoteState } = this;
|
||||
const quotedText = Quote.build(post, quoteState.buffer);
|
||||
quoteState.clear();
|
||||
|
||||
composerController.open({
|
||||
action: Composer.CREATE_TOPIC,
|
||||
|
@ -818,7 +817,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
|
||||
postSelected(post) {
|
||||
if (this.get('allPostsSelected')) { return true; }
|
||||
if (this.get('selectedPosts').contains(post)) { return true; }
|
||||
if (this.get('selectedPosts').includes(post)) { return true; }
|
||||
if (this.get('selectedReplies').findBy('post_number', post.get('reply_to_post_number'))) { return true; }
|
||||
|
||||
return false;
|
||||
|
|
|
@ -87,7 +87,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
if (!Ember.isEmpty(siteUserFields)) {
|
||||
const userFields = this.get('model.user_fields');
|
||||
return siteUserFields.filterBy('show_on_profile', true).sortBy('position').map(field => {
|
||||
field.dasherized_name = field.get('name').dasherize();
|
||||
Ember.set(field, 'dasherized_name', field.get('name').dasherize());
|
||||
const value = userFields ? userFields[field.get('id').toString()] : null;
|
||||
return Ember.isEmpty(value) ? null : Ember.Object.create({ value, field });
|
||||
}).compact();
|
||||
|
|
|
@ -11,9 +11,9 @@ registerUnbound('number', (orig, params) => {
|
|||
orig = parseInt(orig, 10);
|
||||
if (isNaN(orig)) { orig = 0; }
|
||||
|
||||
let title = orig;
|
||||
let title = I18n.toNumber(orig, { precision: 0 });
|
||||
if (params.numberKey) {
|
||||
title = I18n.t(params.numberKey, { number: orig });
|
||||
title = I18n.t(params.numberKey, { number: title, count: parseInt(title) });
|
||||
}
|
||||
|
||||
let classNames = 'number';
|
||||
|
|
|
@ -2,6 +2,9 @@ import { categoryLinkHTML } from 'discourse/helpers/category-link';
|
|||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
|
||||
registerUnbound('category-badge', function(cat, options) {
|
||||
options.link = false;
|
||||
return categoryLinkHTML(cat, options);
|
||||
return categoryLinkHTML(cat, {
|
||||
hideParent: options.hideParent,
|
||||
allowUncategorized: options.allowUncategorized,
|
||||
link: false
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
const { registerKeyword } = Ember.__loader.require("ember-htmlbars/keywords");
|
||||
const { internal } = Ember.__loader.require('htmlbars-runtime');
|
||||
import PreloadStore from 'preload-store';
|
||||
|
||||
let _customizations = {};
|
||||
|
@ -24,36 +22,3 @@ export function clearHTMLCache() {
|
|||
export function setCustomHTML(key, html) {
|
||||
_customizations[key] = html;
|
||||
}
|
||||
|
||||
registerKeyword('custom-html', {
|
||||
setupState(state, env, scope, params) {
|
||||
return { htmlKey: env.hooks.getValue(params[0]) };
|
||||
},
|
||||
|
||||
render(renderNode, env, scope, params, hash, template, inverse, visitor) {
|
||||
let state = renderNode.getState();
|
||||
if (!state.htmlKey) { return true; }
|
||||
|
||||
const html = getCustomHTML(state.htmlKey);
|
||||
if (html) {
|
||||
const htmlHash = { html };
|
||||
env.hooks.component(renderNode,
|
||||
env,
|
||||
scope,
|
||||
'custom-html-container',
|
||||
params,
|
||||
htmlHash,
|
||||
{ default: template, inverse },
|
||||
visitor);
|
||||
return true;
|
||||
}
|
||||
|
||||
template = env.owner.lookup(`template:${state.htmlKey}`);
|
||||
if (template) {
|
||||
internal.hostBlock(renderNode, env, scope, template.raw, null, null, visitor, function(options) {
|
||||
options.templates.template.yield();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
|
||||
registerUnbound('max-usernames', function(usernames, params) {
|
||||
var maxLength = parseInt(params.max) || 3;
|
||||
if (usernames.length > maxLength){
|
||||
return usernames.slice(0, maxLength).join(", ") + ", +" + (usernames.length - maxLength);
|
||||
} else {
|
||||
return usernames.join(", ");
|
||||
}
|
||||
});
|
|
@ -1,138 +0,0 @@
|
|||
/**
|
||||
A plugin outlet is an extension point for templates where other templates can
|
||||
be inserted by plugins.
|
||||
|
||||
## Usage
|
||||
|
||||
If your handlebars template has:
|
||||
|
||||
```handlebars
|
||||
{{plugin-outlet "evil-trout"}}
|
||||
```
|
||||
|
||||
Then any handlebars files you create in the `connectors/evil-trout` directory
|
||||
will automatically be appended. For example:
|
||||
|
||||
plugins/hello/assets/javascripts/discourse/templates/connectors/evil-trout/hello.hbs
|
||||
|
||||
With the contents:
|
||||
|
||||
```handlebars
|
||||
<b>Hello World</b>
|
||||
```
|
||||
|
||||
Will insert <b>Hello World</b> at that point in the template.
|
||||
|
||||
## Disabling
|
||||
|
||||
If a plugin returns a disabled status, the outlets will not be wired up for it.
|
||||
The list of disabled plugins is returned via the `Site` singleton.
|
||||
|
||||
**/
|
||||
let _connectorCache, _templateCache;
|
||||
|
||||
function findOutlets(collection, callback) {
|
||||
const disabledPlugins = Discourse.Site.currentProp('disabled_plugins') || [];
|
||||
|
||||
Object.keys(collection).forEach(function(res) {
|
||||
if (res.indexOf("/connectors/") !== -1) {
|
||||
// Skip any disabled plugins
|
||||
for (let i=0; i<disabledPlugins.length; i++) {
|
||||
if (res.indexOf("/" + disabledPlugins[i] + "/") !== -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const segments = res.split("/");
|
||||
let outletName = segments[segments.length-2];
|
||||
const uniqueName = segments[segments.length-1];
|
||||
|
||||
callback(outletName, res, uniqueName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function clearCache() {
|
||||
_templateCache = null;
|
||||
_connectorCache = null;
|
||||
}
|
||||
|
||||
function buildConnectorCache() {
|
||||
_connectorCache = {};
|
||||
_templateCache = [];
|
||||
|
||||
findOutlets(Ember.TEMPLATES, function(outletName, resource, uniqueName) {
|
||||
_connectorCache[outletName] = _connectorCache[outletName] || [];
|
||||
|
||||
_connectorCache[outletName].push({
|
||||
templateName: resource.replace('javascripts/', ''),
|
||||
template: Ember.TEMPLATES[resource],
|
||||
classNames: `${outletName}-outlet ${uniqueName}`
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(_connectorCache).forEach(outletName => {
|
||||
const connector = _connectorCache[outletName];
|
||||
(connector || []).forEach(s => {
|
||||
_templateCache.push(s.template);
|
||||
s.templateId = parseInt(_templateCache.length - 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// unbound version of outlets, only has a template
|
||||
Handlebars.registerHelper('plugin-outlet', function(name) {
|
||||
if (!_connectorCache) { buildConnectorCache(); }
|
||||
|
||||
const connector = _connectorCache[name];
|
||||
if (connector && connector.length) {
|
||||
const output = connector.map(c => c.template({context: this}));
|
||||
return new Handlebars.SafeString(output.join(""));
|
||||
}
|
||||
});
|
||||
|
||||
const { registerKeyword } = Ember.__loader.require("ember-htmlbars/keywords");
|
||||
const { internal } = Ember.__loader.require('htmlbars-runtime');
|
||||
|
||||
registerKeyword('plugin-outlet', {
|
||||
setupState(state, env, scope, params) {
|
||||
if (!_connectorCache) { buildConnectorCache(); }
|
||||
return { outletName: env.hooks.getValue(params[0]) };
|
||||
},
|
||||
|
||||
render(renderNode, env, scope, params, hash, template, inverse, visitor) {
|
||||
let state = renderNode.getState();
|
||||
if (!state.outletName) { return true; }
|
||||
const connector = _connectorCache[state.outletName];
|
||||
if (!connector || connector.length === 0) { return true; }
|
||||
|
||||
const listTemplate = Ember.TEMPLATES['outlet-list'];
|
||||
listTemplate.raw.locals = ['templateId', 'outletClasses', 'tagName'];
|
||||
|
||||
internal.hostBlock(renderNode, env, scope, listTemplate.raw, null, null, visitor, function(options) {
|
||||
connector.forEach(source => {
|
||||
const tid = source.templateId;
|
||||
options.templates.template.yieldItem(`d-outlet-${tid}`, [
|
||||
tid,
|
||||
source.classNames,
|
||||
hash.tagName || 'div'
|
||||
]);
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
registerKeyword('connector', function(morph, env, scope, params, hash, template, inverse, visitor) {
|
||||
template = _templateCache[parseInt(env.hooks.getValue(hash.templateId))];
|
||||
|
||||
env.hooks.component(morph,
|
||||
env,
|
||||
scope,
|
||||
'connector-container',
|
||||
params,
|
||||
hash,
|
||||
{ default: template.raw, inverse },
|
||||
visitor);
|
||||
return true;
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { rawConnectorsFor } from 'discourse/lib/plugin-connectors';
|
||||
|
||||
Handlebars.registerHelper('raw-plugin-outlet', function(args) {
|
||||
const connectors = rawConnectorsFor(args.hash.name);
|
||||
if (connectors.length) {
|
||||
const output = connectors.map(c => c.template({context: this}));
|
||||
return new Handlebars.SafeString(output.join(""));
|
||||
}
|
||||
});
|
|
@ -1,8 +1,10 @@
|
|||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
let _injections;
|
||||
|
||||
function renderRaw(ctx, container, template, templateName, params) {
|
||||
params = jQuery.extend({}, params);
|
||||
params.parent = params.parent || ctx;
|
||||
|
||||
if (!params.view) {
|
||||
|
@ -32,9 +34,9 @@ registerUnbound('raw', function(templateName, params) {
|
|||
templateName = templateName.replace('.', '/');
|
||||
|
||||
const container = Discourse.__container__;
|
||||
var template = container.lookup('template:' + templateName + '.raw');
|
||||
const template = findRawTemplate(templateName);
|
||||
if (!template) {
|
||||
Ember.warn('Could not find raw template: ' + templateName);
|
||||
console.warn('Could not find raw template: ' + templateName);
|
||||
return;
|
||||
}
|
||||
return renderRaw(this, container, template, templateName, params);
|
||||
|
|
|
@ -8,6 +8,18 @@ export default {
|
|||
withPluginApi('0.1', api => {
|
||||
api.decorateCooked(highlightSyntax);
|
||||
api.decorateCooked(lightbox);
|
||||
|
||||
api.decorateCooked($elem => {
|
||||
const players = $('audio', $elem);
|
||||
if (players.length) {
|
||||
players.on('play', () => {
|
||||
const postId = parseInt($elem.closest('article').data('post-id'));
|
||||
if (postId) {
|
||||
api.preventCloak(postId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import groups from 'discourse/lib/emoji/groups';
|
|||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojiList } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
const keyValueStore = new KeyValueStore("discourse_emojis_");
|
||||
const EMOJI_USAGE = "emojiUsage";
|
||||
|
@ -151,7 +152,7 @@ function render(page, offset, options) {
|
|||
};
|
||||
|
||||
$('.emoji-modal', options.appendTo).remove();
|
||||
const template = options.register.lookup('template:emoji-toolbar.raw');
|
||||
const template = findRawTemplate('emoji-toolbar');
|
||||
options.appendTo.append(template(model));
|
||||
|
||||
bindEvents(page, offset, options);
|
||||
|
|
|
@ -5,13 +5,16 @@ const _loading = {};
|
|||
function loadWithTag(path, cb) {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
|
||||
let finished = false;
|
||||
let s = document.createElement('script');
|
||||
s.src = path;
|
||||
if (Ember.Test) { Ember.Test.pendingAjaxRequests++; }
|
||||
if (Ember.Test) {
|
||||
Ember.Test.registerWaiter(() => finished);
|
||||
}
|
||||
head.appendChild(s);
|
||||
|
||||
s.onload = s.onreadystatechange = function(_, abort) {
|
||||
if (Ember.Test) { Ember.Test.pendingAjaxRequests--; }
|
||||
finished = true;
|
||||
if (abort || !s.readyState || s.readyState === "loaded" || s.readyState === "complete") {
|
||||
s = s.onload = s.onreadystatechange = null;
|
||||
if (!abort) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { preventCloak } from 'discourse/widgets/post-stream';
|
|||
import { h } from 'virtual-dom';
|
||||
import { addFlagProperty } from 'discourse/components/site-header';
|
||||
import { addPopupMenuOptionsCallback } from 'discourse/controllers/composer';
|
||||
import { extraConnectorClass } from 'discourse/lib/plugin-connectors';
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
|
@ -330,15 +331,41 @@ class PluginApi {
|
|||
addStorePluralization(thing, plural) {
|
||||
this.container.lookup("store:main").addPluralization(thing, plural);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a Connector class for a particular outlet and connector.
|
||||
*
|
||||
* For example, if the outlet is `user-profile-primary` and your connector
|
||||
* template is called `my-connector.hbs`:
|
||||
*
|
||||
* ```javascript
|
||||
* api.registerConnectorClass('user-profile-primary', 'my-connector', {
|
||||
* shouldRender(args, component) {
|
||||
* return component.siteSettings.my_plugin_enabled;
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* For more information on connector classes, see:
|
||||
* https://meta.discourse.org/t/important-changes-to-plugin-outlets-for-ember-2-10/54136
|
||||
**/
|
||||
registerConnectorClass(outletName, connectorName, klass) {
|
||||
extraConnectorClass(`${outletName}/${connectorName}`, klass);
|
||||
}
|
||||
}
|
||||
|
||||
let _pluginv01;
|
||||
function getPluginApi(version) {
|
||||
version = parseFloat(version);
|
||||
if (version <= 0.5) {
|
||||
if (version <= 0.6) {
|
||||
if (!_pluginv01) {
|
||||
_pluginv01 = new PluginApi(version, Discourse.__container__);
|
||||
}
|
||||
|
||||
// We are recycling the compatible object, but let's update to the higher version
|
||||
if (_pluginv01.version < version) {
|
||||
_pluginv01.version = version;
|
||||
}
|
||||
return _pluginv01;
|
||||
} else {
|
||||
console.warn(`Plugin API v${version} is not supported`);
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
let _connectorCache;
|
||||
let _rawConnectorCache;
|
||||
let _extraConnectorClasses = {};
|
||||
let _classPaths;
|
||||
|
||||
export function resetExtraClasses() {
|
||||
_extraConnectorClasses = {};
|
||||
_classPaths = undefined;
|
||||
}
|
||||
|
||||
// Note: In plugins, define a class by path and it will be wired up automatically
|
||||
// eg: discourse/connectors/<OUTLET NAME>/<CONNECTOR NAME>.js.es6
|
||||
export function extraConnectorClass(name, obj) {
|
||||
_extraConnectorClasses[name] = obj;
|
||||
}
|
||||
|
||||
const DefaultConnectorClass = {
|
||||
actions: {},
|
||||
shouldRender: () => true,
|
||||
setupComponent() { }
|
||||
};
|
||||
|
||||
function findOutlets(collection, callback) {
|
||||
const disabledPlugins = Discourse.Site.currentProp('disabled_plugins') || [];
|
||||
|
||||
Object.keys(collection).forEach(function(res) {
|
||||
if (res.indexOf("/connectors/") !== -1) {
|
||||
// Skip any disabled plugins
|
||||
for (let i=0; i<disabledPlugins.length; i++) {
|
||||
if (res.indexOf("/" + disabledPlugins[i] + "/") !== -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const segments = res.split("/");
|
||||
let outletName = segments[segments.length-2];
|
||||
const uniqueName = segments[segments.length-1];
|
||||
|
||||
callback(outletName, res, uniqueName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function clearCache() {
|
||||
_connectorCache = null;
|
||||
_rawConnectorCache = null;
|
||||
}
|
||||
|
||||
function findClass(outletName, uniqueName) {
|
||||
if (!_classPaths) {
|
||||
_classPaths = {};
|
||||
findOutlets(require._eak_seen, (outlet, res, un) => {
|
||||
_classPaths[`${outlet}/${un}`] = require(res).default;
|
||||
});
|
||||
}
|
||||
|
||||
const id = `${outletName}/${uniqueName}`;
|
||||
let foundClass = _extraConnectorClasses[id] || _classPaths[id];
|
||||
|
||||
return foundClass ?
|
||||
jQuery.extend({}, DefaultConnectorClass, foundClass) :
|
||||
DefaultConnectorClass;
|
||||
}
|
||||
|
||||
function buildConnectorCache() {
|
||||
_connectorCache = {};
|
||||
|
||||
findOutlets(Ember.TEMPLATES, (outletName, resource, uniqueName) => {
|
||||
_connectorCache[outletName] = _connectorCache[outletName] || [];
|
||||
|
||||
_connectorCache[outletName].push({
|
||||
templateName: resource.replace('javascripts/', ''),
|
||||
template: Ember.TEMPLATES[resource],
|
||||
classNames: `${outletName}-outlet ${uniqueName}`,
|
||||
connectorClass: findClass(outletName, uniqueName)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildRawConnectorCache() {
|
||||
_rawConnectorCache = {};
|
||||
findOutlets(Discourse.RAW_TEMPLATES, (outletName, resource) => {
|
||||
_rawConnectorCache[outletName] = _rawConnectorCache[outletName] || [];
|
||||
_rawConnectorCache[outletName].push({
|
||||
template: Discourse.RAW_TEMPLATES[resource]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function connectorsFor(outletName) {
|
||||
if (!_connectorCache) { buildConnectorCache(); }
|
||||
return _connectorCache[outletName] || [];
|
||||
}
|
||||
|
||||
export function rawConnectorsFor(outletName) {
|
||||
if (!_rawConnectorCache) { buildRawConnectorCache(); }
|
||||
return _rawConnectorCache[outletName] || [];
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export default class QuoteState {
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
selected(postId, buffer) {
|
||||
this.postId = postId;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.buffer = '';
|
||||
this.postId = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { getResolverOption } from 'discourse-common/resolver';
|
||||
|
||||
export function findRawTemplate(name) {
|
||||
if (getResolverOption('mobileView')) {
|
||||
return Discourse.RAW_TEMPLATES[`javascripts/mobile/${name}`] ||
|
||||
Discourse.RAW_TEMPLATES[`javascripts/${name}`] ||
|
||||
Discourse.RAW_TEMPLATES[`mobile/${name}`] ||
|
||||
Discourse.RAW_TEMPLATES[name];
|
||||
}
|
||||
|
||||
return Discourse.RAW_TEMPLATES[`javascripts/${name}`] ||
|
||||
Discourse.RAW_TEMPLATES[name];
|
||||
}
|
|
@ -5,7 +5,7 @@ export default function renderTag(tag, params) {
|
|||
tag = Handlebars.Utils.escapeExpression(tag);
|
||||
const classes = ['tag-' + tag, 'discourse-tag'];
|
||||
const tagName = params.tagName || "a";
|
||||
const href = tagName === "a" ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : "";
|
||||
const href = (tagName === "a" && !params.noHref) ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : "";
|
||||
|
||||
if (Discourse.SiteSettings.tag_style || params.style) {
|
||||
classes.push(params.style || Discourse.SiteSettings.tag_style);
|
||||
|
|
|
@ -6,6 +6,23 @@ function applicable() {
|
|||
!navigator.userAgent.match(/Trident/g);
|
||||
}
|
||||
|
||||
|
||||
function calcHeight(composingTopic) {
|
||||
const winHeight = window.innerHeight;
|
||||
|
||||
// Hard code some known iOS resolutions
|
||||
switch(winHeight) {
|
||||
case 460: return composingTopic ? 250 : 260;
|
||||
case 559: return composingTopic ? 325 : 308;
|
||||
case 627:
|
||||
case 628: return 360;
|
||||
}
|
||||
|
||||
const ratio = composingTopic ? 0.54 : 0.6;
|
||||
const min = composingTopic ? 300 : 350;
|
||||
return Math.max(parseInt(winHeight*ratio), min);
|
||||
}
|
||||
|
||||
let workaroundActive = false;
|
||||
let composingTopic = false;
|
||||
|
||||
|
@ -86,19 +103,9 @@ function positioningWorkaround($fixedElement) {
|
|||
|
||||
fixedElement.style.top = '0px';
|
||||
|
||||
let ratio = 0.6;
|
||||
let min = 350;
|
||||
composingTopic = $('#reply-control select.category-combobox').length > 0;
|
||||
|
||||
composingTopic = false;
|
||||
|
||||
if ($('#reply-control select.category-combobox').length > 0) {
|
||||
composingTopic = true;
|
||||
// creating a topic, less height
|
||||
ratio = 0.54;
|
||||
min = 300;
|
||||
}
|
||||
|
||||
const height = Math.max(parseInt(window.innerHeight*ratio), min);
|
||||
const height = calcHeight(composingTopic);
|
||||
|
||||
fixedElement.style.height = height + "px";
|
||||
|
||||
|
|
|
@ -11,11 +11,7 @@ export default function(name, opts) {
|
|||
|
||||
const controllerName = opts.admin ? `modals/${name}` : name;
|
||||
|
||||
const viewClass = container.lookupFactory('view:' + name);
|
||||
const controller = container.lookup('controller:' + controllerName);
|
||||
if (viewClass) {
|
||||
route.render(name, { into: 'modal', outlet: 'modalBody' });
|
||||
} else {
|
||||
const templateName = opts.templateName || Ember.String.dasherize(name);
|
||||
|
||||
const renderArgs = { into: 'modal', outlet: 'modalBody'};
|
||||
|
@ -25,13 +21,13 @@ export default function(name, opts) {
|
|||
renderArgs.view = 'modal-body';
|
||||
}
|
||||
|
||||
|
||||
const modalName = `modal/${templateName}`;
|
||||
const fullName = opts.admin ? `admin/templates/${modalName}` : modalName;
|
||||
route.render(fullName, renderArgs);
|
||||
if (opts.title) {
|
||||
modalController.set('title', I18n.t(opts.title));
|
||||
}
|
||||
}
|
||||
|
||||
if (controller) {
|
||||
controller.set('modal', modalController);
|
||||
|
|
|
@ -29,8 +29,8 @@ function emojiOptions() {
|
|||
return { getURL: Discourse.getURLWithCDN, emojiSet: siteSettings.emoji_set };
|
||||
}
|
||||
|
||||
export function emojiUnescape(string) {
|
||||
const opts = emojiOptions();
|
||||
export function emojiUnescape(string, options) {
|
||||
const opts = _.extend(emojiOptions(), options || {});
|
||||
return opts ? performEmojiUnescape(string, opts) : string;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,15 @@ const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;
|
|||
|
||||
// We can add links here that have server side responses but not client side.
|
||||
const SERVER_SIDE_ONLY = [
|
||||
/^\/assets\//,
|
||||
/^\/uploads\//,
|
||||
/^\/stylesheets\//,
|
||||
/^\/site_customizations\//,
|
||||
/^\/raw\//,
|
||||
/^\/posts\/\d+\/raw/,
|
||||
/^\/raw\/\d+/,
|
||||
/\.rss$/,
|
||||
/\.json/,
|
||||
/\.json$/,
|
||||
];
|
||||
|
||||
let _jumpScheduled = false;
|
||||
|
@ -121,8 +127,9 @@ const DiscourseURL = Ember.Object.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const pathname = path.replace(/(https?\:)?\/\/[^\/]+/, '');
|
||||
const serverSide = SERVER_SIDE_ONLY.some(r => {
|
||||
if (path.match(r)) {
|
||||
if (pathname.match(r)) {
|
||||
document.location = path;
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -158,7 +158,7 @@ export function setCaretPosition(ctrl, pos) {
|
|||
}
|
||||
}
|
||||
|
||||
export function validateUploadedFiles(files, bypassNewUserRestriction) {
|
||||
export function validateUploadedFiles(files, opts) {
|
||||
if (!files || files.length === 0) { return false; }
|
||||
|
||||
if (files.length > 1) {
|
||||
|
@ -166,29 +166,48 @@ export function validateUploadedFiles(files, bypassNewUserRestriction) {
|
|||
return false;
|
||||
}
|
||||
|
||||
var upload = files[0];
|
||||
const upload = files[0];
|
||||
|
||||
// CHROME ONLY: if the image was pasted, sets its name to a default one
|
||||
if (typeof Blob !== "undefined" && typeof File !== "undefined") {
|
||||
if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
|
||||
}
|
||||
|
||||
var type = uploadTypeFromFileName(upload.name);
|
||||
opts = opts || {};
|
||||
opts["type"] = uploadTypeFromFileName(upload.name);
|
||||
|
||||
return validateUploadedFile(upload, type, bypassNewUserRestriction);
|
||||
return validateUploadedFile(upload, opts);
|
||||
}
|
||||
|
||||
export function validateUploadedFile(file, type, bypassNewUserRestriction) {
|
||||
export function validateUploadedFile(file, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
const name = file && file.name;
|
||||
|
||||
if (!name) { return false; }
|
||||
|
||||
// check that the uploaded file is authorized
|
||||
if (!authorizesAllExtensions() && !isAuthorizedUpload(file)) {
|
||||
if (opts["imagesOnly"]) {
|
||||
if (!isAnImage(name) && !isAuthorizedImage(name)) {
|
||||
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedImagesExtensions() }));
|
||||
return false;
|
||||
}
|
||||
} else if (opts["csvOnly"]) {
|
||||
if (!(/\.csv$/i).test(name)) {
|
||||
bootbox.alert(I18n.t('user.invited.bulk_invite.error'));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!authorizesAllExtensions() && !isAuthorizedFile(name)) {
|
||||
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedExtensions() }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bypassNewUserRestriction) {
|
||||
if (!opts["bypassNewUserRestriction"]) {
|
||||
// ensures that new users can upload a file
|
||||
if (!Discourse.User.current().isAllowedToUploadAFile(type)) {
|
||||
bootbox.alert(I18n.t('post.errors.' + type + '_upload_not_allowed_for_new_user'));
|
||||
if (!Discourse.User.current().isAllowedToUploadAFile(opts["type"])) {
|
||||
bootbox.alert(I18n.t(`post.errors.${opts["type"]}_upload_not_allowed_for_new_user`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -197,13 +216,7 @@ export function validateUploadedFile(file, type, bypassNewUserRestriction) {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function uploadTypeFromFileName(fileName) {
|
||||
return isAnImage(fileName) ? 'image' : 'attachment';
|
||||
}
|
||||
|
||||
export function authorizesAllExtensions() {
|
||||
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
|
||||
}
|
||||
const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i;
|
||||
|
||||
function extensions() {
|
||||
return Discourse.SiteSettings.authorized_extensions
|
||||
|
@ -213,16 +226,52 @@ function extensions() {
|
|||
.filter(ext => ext.indexOf("*") === -1);
|
||||
}
|
||||
|
||||
function imagesExtensions() {
|
||||
return extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
|
||||
}
|
||||
|
||||
function extensionsRegex() {
|
||||
return new RegExp("\\.(" + extensions().join("|") + ")$", "i");
|
||||
}
|
||||
|
||||
export function isAuthorizedUpload(file) {
|
||||
return file && file.name && extensionsRegex().test(file.name);
|
||||
function imagesExtensionsRegex() {
|
||||
return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i");
|
||||
}
|
||||
|
||||
function isAuthorizedFile(fileName) {
|
||||
return extensionsRegex().test(fileName);
|
||||
}
|
||||
|
||||
function isAuthorizedImage(fileName){
|
||||
return imagesExtensionsRegex().test(fileName);
|
||||
}
|
||||
|
||||
export function authorizedExtensions() {
|
||||
return extensions().join(", ");
|
||||
return authorizesAllExtensions() ? "*" : extensions().join(", ");
|
||||
}
|
||||
|
||||
export function authorizedImagesExtensions() {
|
||||
return authorizesAllExtensions() ? "png, jpg, jpeg, gif, bmp, tiff, svg, webp, ico" : imagesExtensions().join(", ");
|
||||
}
|
||||
|
||||
export function authorizesAllExtensions() {
|
||||
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
|
||||
}
|
||||
|
||||
export function isAnImage(path) {
|
||||
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)$/i).test(path);
|
||||
}
|
||||
|
||||
function uploadTypeFromFileName(fileName) {
|
||||
return isAnImage(fileName) ? 'image' : 'attachment';
|
||||
}
|
||||
|
||||
export function allowsImages() {
|
||||
return authorizesAllExtensions() || IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions());
|
||||
}
|
||||
|
||||
export function allowsAttachments() {
|
||||
return authorizesAllExtensions() || extensions().length > imagesExtensions().length;
|
||||
}
|
||||
|
||||
export function uploadLocation(url) {
|
||||
|
@ -243,27 +292,12 @@ export function getUploadMarkdown(upload) {
|
|||
if (isAnImage(upload.original_filename)) {
|
||||
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
|
||||
} else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i).test(upload.original_filename)) {
|
||||
// is Audio/Video
|
||||
return uploadLocation(upload.url);
|
||||
} else {
|
||||
return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a> (' + I18n.toHumanSize(upload.filesize) + ')\n';
|
||||
}
|
||||
}
|
||||
|
||||
export function isAnImage(path) {
|
||||
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)$/i).test(path);
|
||||
}
|
||||
|
||||
export function allowsImages() {
|
||||
return authorizesAllExtensions() ||
|
||||
(/(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i).test(authorizedExtensions());
|
||||
}
|
||||
|
||||
export function allowsAttachments() {
|
||||
return authorizesAllExtensions() ||
|
||||
!/^((png|jpe?g|gif|bmp|tiff?|svg|webp|ico)(,\s)?)+$/i.test(authorizedExtensions());
|
||||
}
|
||||
|
||||
export function displayErrorForUpload(data) {
|
||||
// deal with meaningful errors first
|
||||
if (data.jqXHR) {
|
||||
|
@ -300,5 +334,35 @@ export function defaultHomepage() {
|
|||
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
|
||||
}
|
||||
|
||||
export function determinePostReplaceSelection({ selection, needle, replacement }) {
|
||||
const diff = (replacement.end - replacement.start) - (needle.end - needle.start);
|
||||
|
||||
if (selection.end <= needle.start) {
|
||||
// Selection ends (and starts) before needle.
|
||||
return { start: selection.start, end: selection.end };
|
||||
} else if (selection.start <= needle.start) {
|
||||
// Selection starts before needle...
|
||||
if (selection.end < needle.end) {
|
||||
// ... and ends inside needle.
|
||||
return { start: selection.start, end: needle.start };
|
||||
} else {
|
||||
// ... and spans needle completely.
|
||||
return { start: selection.start, end: selection.end + diff };
|
||||
}
|
||||
} else if (selection.start < needle.end) {
|
||||
// Selection starts inside needle...
|
||||
if (selection.end <= needle.end) {
|
||||
// ... and ends inside needle.
|
||||
return { start: replacement.end, end: replacement.end };
|
||||
} else {
|
||||
// ... and spans end of needle.
|
||||
return { start: replacement.end, end: selection.end + diff };
|
||||
}
|
||||
} else {
|
||||
// Selection starts (and ends) behind needle.
|
||||
return { start: selection.start + diff, end: selection.end + diff };
|
||||
}
|
||||
}
|
||||
|
||||
// This prevents a mini racer crash
|
||||
export default {};
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
const rootURL = Discourse.BaseUri;
|
||||
|
||||
const BareRouter = Ember.Router.extend({
|
||||
rootURL,
|
||||
location: Ember.testing ? 'none': 'discourse-location'
|
||||
location: Ember.testing ? 'none': 'discourse-location',
|
||||
|
||||
handleURL(url) {
|
||||
const params = url.split('?');
|
||||
|
||||
if (params[0] === "/") {
|
||||
url = defaultHomepage();
|
||||
if (params[1] && params[1].length) {
|
||||
url = `${url}?${params[1]}`;
|
||||
}
|
||||
}
|
||||
return this._super(url);
|
||||
}
|
||||
});
|
||||
|
||||
// Ember's router can't be extended. We need to allow plugins to add routes to routes that were defined
|
||||
|
@ -67,7 +80,8 @@ class RouteNode {
|
|||
if (paths.length > 1) {
|
||||
paths.filter(p => p !== this.opts.path).forEach(path => {
|
||||
const newOpts = jQuery.extend({}, this.opts, { path });
|
||||
router.route(this.name, newOpts, builder);
|
||||
console.log(`warning: we can't have duplicate route names anymore`, newOpts);
|
||||
// router.route(this.name, newOpts, builder);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue