mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
Merge remote-tracking branch 'upstream/master' into update-locale-vietnamese
* upstream/master: (185 commits) SECURITY: Upgrade rails. FIX: new user summary page was broken Version bump to v1.5.0.beta9 Remove addressable from Discourse. UX: change glyph when inviting existing user to a topic FIX: Allow for large free disk space Revert "FIX: disk_space should be a BigDecimal to handle large disk (closes #3923)" UX: improve styling of messages and mobile view of messages FIX: correct counts on user summary FIX: link to filtered down list of badges from summary FEATURE: pick featured badges in summary page FIX: do not allow new email to be duplicate FIX: return proper error message when email already exists retain unactivated accounts a bit longer default FEATURE: blocked users can send and reply to private messages from staff Remove Arel patch that has been merged upstream. correct path little typo FIX: Missing tag in CSS. PERF: remove 10-20ms of work from every page view FIX: remove green background for wiki (this can be re-added via a customization if needed) Hotfix for unsubscribe via email ... # Conflicts: # .tx/config
This commit is contained in:
commit
06e637fc4a
|
@ -1,6 +1,6 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, vi_VN: vi
|
||||
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
|
||||
|
||||
[discourse-org.clientenyml]
|
||||
file_filter = config/locales/client.<lang>.yml
|
||||
|
|
10
Gemfile
10
Gemfile
|
@ -46,11 +46,11 @@ gem 'active_model_serializers', '~> 0.8.3'
|
|||
gem 'onebox'
|
||||
|
||||
gem 'ember-rails'
|
||||
gem 'ember-source', '1.12.1'
|
||||
gem 'ember-source', '1.12.2'
|
||||
gem 'barber'
|
||||
gem 'babel-transpiler'
|
||||
|
||||
gem 'message_bus'
|
||||
gem 'message_bus', '2.0.0.beta.2'
|
||||
|
||||
gem 'rails_multisite'
|
||||
|
||||
|
@ -83,7 +83,9 @@ gem 'omniauth-twitter'
|
|||
gem 'omniauth-github-discourse', require: 'omniauth-github'
|
||||
|
||||
gem 'omniauth-oauth2', require: false
|
||||
gem 'omniauth-google-oauth2'
|
||||
|
||||
# this removes the dependency on 'addressable'
|
||||
gem 'omniauth-google-oauth2', git: 'git://github.com/zquestz/omniauth-google-oauth2.git', ref: 'b492c4bb8286d35'
|
||||
gem 'oj'
|
||||
gem 'pg'
|
||||
gem 'pry-rails', require: false
|
||||
|
@ -183,7 +185,7 @@ 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
|
||||
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]
|
||||
|
|
181
Gemfile.lock
181
Gemfile.lock
|
@ -1,54 +1,65 @@
|
|||
GIT
|
||||
remote: git://github.com/zquestz/omniauth-google-oauth2.git
|
||||
revision: b492c4bb8286d35ae1168d7f2e5c57769bfe45a0
|
||||
ref: b492c4bb8286d35
|
||||
specs:
|
||||
omniauth-google-oauth2 (0.3.0)
|
||||
jwt (~> 1.0)
|
||||
multi_json (~> 1.3)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.3.1)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (4.2.5)
|
||||
actionpack (= 4.2.5)
|
||||
actionview (= 4.2.5)
|
||||
activejob (= 4.2.5)
|
||||
actionmailer (4.2.5.1)
|
||||
actionpack (= 4.2.5.1)
|
||||
actionview (= 4.2.5.1)
|
||||
activejob (= 4.2.5.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
actionpack (4.2.5)
|
||||
actionview (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
actionpack (4.2.5.1)
|
||||
actionview (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
rack (~> 1.6)
|
||||
rack-test (~> 0.6.2)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
actionview (4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
active_model_serializers (0.8.3)
|
||||
activemodel (>= 3.0)
|
||||
activejob (4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
activejob (4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
activemodel (4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.5)
|
||||
activemodel (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
activerecord (4.2.5.1)
|
||||
activemodel (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.5)
|
||||
activesupport (4.2.5.1)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
annotate (2.6.10)
|
||||
activerecord (>= 3.2, <= 4.3)
|
||||
annotate (2.7.0)
|
||||
activerecord (>= 3.2, < 6.0)
|
||||
rake (~> 10.4)
|
||||
arel (6.0.3)
|
||||
aws-sdk (2.1.29)
|
||||
aws-sdk-resources (= 2.1.29)
|
||||
aws-sdk-core (2.1.29)
|
||||
aws-sdk (2.2.9)
|
||||
aws-sdk-resources (= 2.2.9)
|
||||
aws-sdk-core (2.2.9)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-resources (2.1.29)
|
||||
aws-sdk-core (= 2.1.29)
|
||||
babel-source (5.8.19)
|
||||
aws-sdk-resources (2.2.9)
|
||||
aws-sdk-core (= 2.2.9)
|
||||
babel-source (5.8.34)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
execjs (~> 2.0)
|
||||
|
@ -62,7 +73,7 @@ GEM
|
|||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
builder (3.2.2)
|
||||
byebug (6.0.2)
|
||||
byebug (8.2.1)
|
||||
certified (1.0.0)
|
||||
coderay (1.1.0)
|
||||
concurrent-ruby (1.0.0)
|
||||
|
@ -89,15 +100,15 @@ GEM
|
|||
ember-source (>= 1.1.0)
|
||||
jquery-rails (>= 1.0.17)
|
||||
railties (>= 3.1)
|
||||
ember-source (1.12.1)
|
||||
ember-source (1.12.2)
|
||||
erubis (2.7.0)
|
||||
eventmachine (1.0.8)
|
||||
excon (0.45.4)
|
||||
execjs (2.6.0)
|
||||
exifr (1.2.3.1)
|
||||
exifr (1.2.4)
|
||||
fabrication (2.9.8)
|
||||
fakeweb (1.3.0)
|
||||
faraday (0.9.1)
|
||||
faraday (0.9.2)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fast_stack (0.1.0)
|
||||
|
@ -115,15 +126,15 @@ GEM
|
|||
thor (~> 0.19.1)
|
||||
fspath (2.1.1)
|
||||
gctools (0.2.3)
|
||||
given_core (3.5.4)
|
||||
given_core (3.7.1)
|
||||
sorcerer (>= 0.3.7)
|
||||
globalid (0.3.6)
|
||||
activesupport (>= 4.1.0)
|
||||
guess_html_encoding (0.0.11)
|
||||
hashie (3.4.2)
|
||||
highline (1.7.7)
|
||||
hashie (3.4.3)
|
||||
highline (1.7.8)
|
||||
hike (1.2.3)
|
||||
hiredis (0.6.0)
|
||||
hiredis (0.6.1)
|
||||
htmlentities (4.3.4)
|
||||
http-cookie (1.0.2)
|
||||
domain_name (~> 0.5)
|
||||
|
@ -142,12 +153,12 @@ GEM
|
|||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.3)
|
||||
jwt (1.5.1)
|
||||
jwt (1.5.2)
|
||||
kgio (2.10.0)
|
||||
librarian (0.1.2)
|
||||
highline
|
||||
thor (~> 0.15)
|
||||
libv8 (3.16.14.11)
|
||||
libv8 (3.16.14.13)
|
||||
listen (0.7.3)
|
||||
logster (1.0.1)
|
||||
loofah (2.0.3)
|
||||
|
@ -155,28 +166,28 @@ GEM
|
|||
lru_redux (1.1.0)
|
||||
mail (2.6.3)
|
||||
mime-types (>= 1.16, < 3)
|
||||
memory_profiler (0.9.4)
|
||||
message_bus (1.1.1)
|
||||
memory_profiler (0.9.6)
|
||||
message_bus (2.0.0.beta.2)
|
||||
rack (>= 1.1.3)
|
||||
redis
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99)
|
||||
mini_portile2 (2.0.0)
|
||||
minitest (5.8.3)
|
||||
minitest (5.8.4)
|
||||
mocha (1.1.0)
|
||||
metaclass (~> 0.0.1)
|
||||
mock_redis (0.15.2)
|
||||
mock_redis (0.15.4)
|
||||
moneta (0.8.0)
|
||||
msgpack (0.6.2)
|
||||
msgpack (0.7.4)
|
||||
multi_json (1.11.2)
|
||||
multi_xml (0.5.5)
|
||||
multipart-post (2.0.0)
|
||||
mustache (1.0.2)
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.6.7.1)
|
||||
nokogiri (1.6.7.2)
|
||||
mini_portile2 (~> 2.0.0.rc2)
|
||||
nokogumbo (1.4.1)
|
||||
nokogumbo (1.4.7)
|
||||
nokogiri
|
||||
oauth (0.4.7)
|
||||
oauth2 (1.0.0)
|
||||
|
@ -185,18 +196,15 @@ GEM
|
|||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (~> 1.2)
|
||||
oj (2.12.14)
|
||||
omniauth (1.2.2)
|
||||
oj (2.14.3)
|
||||
omniauth (1.3.1)
|
||||
hashie (>= 1.2, < 4)
|
||||
rack (~> 1.0)
|
||||
omniauth-facebook (2.0.1)
|
||||
rack (>= 1.0, < 3)
|
||||
omniauth-facebook (3.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github-discourse (1.1.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-google-oauth2 (0.2.5)
|
||||
omniauth (> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-oauth (1.1.0)
|
||||
oauth
|
||||
omniauth (~> 1.0)
|
||||
|
@ -209,7 +217,7 @@ GEM
|
|||
omniauth-twitter (1.2.1)
|
||||
json (~> 1.3)
|
||||
omniauth-oauth (~> 1.1)
|
||||
onebox (1.5.31)
|
||||
onebox (1.5.33)
|
||||
moneta (~> 0.8)
|
||||
multi_json (~> 1.11)
|
||||
mustache
|
||||
|
@ -217,9 +225,9 @@ GEM
|
|||
openid-redis-store (0.0.2)
|
||||
redis
|
||||
ruby-openid
|
||||
pg (0.18.3)
|
||||
progress (3.1.0)
|
||||
pry (0.10.1)
|
||||
pg (0.18.4)
|
||||
progress (3.1.1)
|
||||
pry (0.10.3)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
|
@ -227,10 +235,10 @@ GEM
|
|||
pry (>= 0.9.10, < 0.11.0)
|
||||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
puma (2.14.0)
|
||||
r2 (0.2.5)
|
||||
puma (2.15.3)
|
||||
r2 (0.2.6)
|
||||
rack (1.6.4)
|
||||
rack-mini-profiler (0.9.7)
|
||||
rack-mini-profiler (0.9.8)
|
||||
rack (>= 1.1.3)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
|
@ -239,16 +247,16 @@ GEM
|
|||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (4.2.5)
|
||||
actionmailer (= 4.2.5)
|
||||
actionpack (= 4.2.5)
|
||||
actionview (= 4.2.5)
|
||||
activejob (= 4.2.5)
|
||||
activemodel (= 4.2.5)
|
||||
activerecord (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
rails (4.2.5.1)
|
||||
actionmailer (= 4.2.5.1)
|
||||
actionpack (= 4.2.5.1)
|
||||
actionview (= 4.2.5.1)
|
||||
activejob (= 4.2.5.1)
|
||||
activemodel (= 4.2.5.1)
|
||||
activerecord (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.2.5)
|
||||
railties (= 4.2.5.1)
|
||||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
|
@ -256,21 +264,21 @@ GEM
|
|||
activesupport (>= 4.2.0.beta, < 5.0)
|
||||
nokogiri (~> 1.6.0)
|
||||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.2)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-observers (0.1.2)
|
||||
activemodel (~> 4.0)
|
||||
rails_multisite (1.0.3)
|
||||
railties (4.2.5)
|
||||
actionpack (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
railties (4.2.5.1)
|
||||
actionpack (= 4.2.5.1)
|
||||
activesupport (= 4.2.5.1)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
raindrops (0.15.0)
|
||||
rake (10.4.2)
|
||||
rake (10.5.0)
|
||||
rake-compiler (0.9.5)
|
||||
rake
|
||||
rb-fsevent (0.9.6)
|
||||
rb-fsevent (0.9.7)
|
||||
rb-inotify (0.9.5)
|
||||
ffi (>= 0.5.0)
|
||||
rbtrace (0.4.7)
|
||||
|
@ -296,16 +304,16 @@ GEM
|
|||
rspec-expectations (3.2.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-given (3.5.4)
|
||||
given_core (= 3.5.4)
|
||||
rspec (>= 2.12)
|
||||
rspec-given (3.7.1)
|
||||
given_core (= 3.7.1)
|
||||
rspec (>= 2.14.0)
|
||||
rspec-html-matchers (0.7.0)
|
||||
nokogiri (~> 1)
|
||||
rspec (~> 3)
|
||||
rspec-mocks (3.2.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-rails (3.2.1)
|
||||
rspec-rails (3.2.3)
|
||||
actionpack (>= 3.0, < 4.3)
|
||||
activesupport (>= 3.0, < 4.3)
|
||||
railties (>= 3.0, < 4.3)
|
||||
|
@ -319,10 +327,10 @@ GEM
|
|||
ruby-readability (0.7.0)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
nokogiri (>= 1.6.0)
|
||||
sanitize (4.0.0)
|
||||
sanitize (4.0.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (= 1.4.1)
|
||||
nokogumbo (~> 1.4.1)
|
||||
sass (3.2.19)
|
||||
sass-rails (4.0.5)
|
||||
railties (>= 4.0.0, < 5.0)
|
||||
|
@ -336,17 +344,16 @@ GEM
|
|||
shoulda-context (~> 1.0, >= 1.0.1)
|
||||
shoulda-matchers (>= 1.4.1, < 3.0)
|
||||
shoulda-context (1.2.1)
|
||||
shoulda-matchers (2.7.0)
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (4.0.1)
|
||||
sidekiq (4.0.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
json (~> 1.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
sidekiq-statistic (1.2.0)
|
||||
sidekiq (>= 3.3.4, < 5)
|
||||
simple-rss (1.3.1)
|
||||
simplecov (0.10.0)
|
||||
simplecov (0.11.1)
|
||||
docile (~> 1.1.0)
|
||||
json (~> 1.8)
|
||||
simplecov-html (~> 0.10.0)
|
||||
|
@ -382,7 +389,7 @@ GEM
|
|||
thread_safe (0.3.5)
|
||||
tilt (1.4.1)
|
||||
timecop (0.8.0)
|
||||
trollop (2.1.1)
|
||||
trollop (2.1.2)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (2.7.2)
|
||||
|
@ -390,8 +397,8 @@ GEM
|
|||
json (>= 1.8.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.6)
|
||||
unicorn (4.9.0)
|
||||
unf_ext (0.0.7.1)
|
||||
unicorn (5.0.1)
|
||||
kgio (~> 2.6)
|
||||
rack
|
||||
raindrops (~> 0.7)
|
||||
|
@ -412,7 +419,7 @@ DEPENDENCIES
|
|||
discourse-qunit-rails
|
||||
discourse_email_parser
|
||||
ember-rails
|
||||
ember-source (= 1.12.1)
|
||||
ember-source (= 1.12.2)
|
||||
excon
|
||||
fabrication (= 2.9.8)
|
||||
fakeweb (~> 1.3.0)
|
||||
|
@ -433,7 +440,7 @@ DEPENDENCIES
|
|||
lru_redux
|
||||
mail
|
||||
memory_profiler
|
||||
message_bus
|
||||
message_bus (= 2.0.0.beta.2)
|
||||
mime-types
|
||||
minitest
|
||||
mocha
|
||||
|
@ -445,7 +452,7 @@ DEPENDENCIES
|
|||
omniauth
|
||||
omniauth-facebook
|
||||
omniauth-github-discourse
|
||||
omniauth-google-oauth2
|
||||
omniauth-google-oauth2!
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
|
|
|
@ -56,7 +56,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
|
|||
|
||||
## Contributing
|
||||
|
||||
[![Build Status](https://travis-ci.org/discourse/discourse.svg)](https://travis-ci.org/discourse/discourse)
|
||||
[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
|
||||
[![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](https://codeclimate.com/github/discourse/discourse)
|
||||
|
||||
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import AdminEmailSkippedController from "admin/controllers/admin-email-skipped";
|
||||
|
||||
export default AdminEmailSkippedController.extend();
|
|
@ -0,0 +1,11 @@
|
|||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
loadMore() {
|
||||
return IncomingEmail.findAll(this.get("filter"), this.get("model.length"))
|
||||
.then(incoming => {
|
||||
if (incoming.length < 50) { this.get("model").set("allLoaded", true); }
|
||||
this.get("model").addObjects(incoming);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
loadMore() {
|
||||
return EmailLog.findAll(this.get("filter"), this.get("model.length"))
|
||||
.then(logs => {
|
||||
if (logs.length < 50) { this.get("model").set("allLoaded", true); }
|
||||
this.get("model").addObjects(logs);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default AdminEmailIncomingsController.extend({
|
||||
filterIncomingEmails: debounce(function() {
|
||||
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
|
||||
}, 250).observes("filter.{from,to,subject}")
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default AdminEmailIncomingsController.extend({
|
||||
filterIncomingEmails: debounce(function() {
|
||||
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
|
||||
}, 250).observes("filter.{from,to,subject,error}")
|
||||
});
|
|
@ -1,12 +1,9 @@
|
|||
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
export default AdminEmailLogsController.extend({
|
||||
filterEmailLogs: debounce(function() {
|
||||
var self = this;
|
||||
EmailLog.findAll(this.get("filter")).then(function(logs) {
|
||||
self.set("model", logs);
|
||||
});
|
||||
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.reply_key")
|
||||
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
|
||||
}, 250).observes("filter.{user,address,type,reply_key}")
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
export default AdminEmailLogsController.extend({
|
||||
filterEmailLogs: debounce(function() {
|
||||
const EmailLog = require('admin/models/email-log').default;
|
||||
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
|
||||
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.skipped_reason")
|
||||
}, 250).observes("filter.{user,address,type,skipped_reason}")
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { exportEntity } from 'discourse/lib/export-csv';
|
||||
import { outputExportResult } from 'discourse/lib/export-result';
|
||||
import Report from 'admin/models/report';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
viewMode: 'table',
|
||||
|
@ -20,9 +21,9 @@ export default Ember.Controller.extend({
|
|||
var q;
|
||||
this.set("refreshing", true);
|
||||
if (this.get('categoryId') === "all") {
|
||||
q = Discourse.Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"));
|
||||
q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"));
|
||||
} else {
|
||||
q = Discourse.Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId"));
|
||||
q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId"));
|
||||
}
|
||||
q.then(m => this.set("model", m)).finally(() => this.set("refreshing", false));
|
||||
},
|
||||
|
|
|
@ -2,15 +2,13 @@ export default Ember.Controller.extend({
|
|||
needs: ['modal'],
|
||||
|
||||
modelChanged: function(){
|
||||
|
||||
var grouping = Em.Object.extend({});
|
||||
|
||||
var model = this.get('model');
|
||||
var copy = Em.A();
|
||||
const model = this.get('model');
|
||||
const copy = Em.A();
|
||||
const store = this.store;
|
||||
|
||||
if(model){
|
||||
model.forEach(function(o){
|
||||
copy.pushObject(grouping.create(o));
|
||||
copy.pushObject(store.createRecord('badge-grouping', o));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -18,8 +16,8 @@ export default Ember.Controller.extend({
|
|||
}.observes('model'),
|
||||
|
||||
moveItem: function(item, delta){
|
||||
var copy = this.get('workingCopy');
|
||||
var index = copy.indexOf(item);
|
||||
const copy = this.get('workingCopy');
|
||||
const index = copy.indexOf(item);
|
||||
if (index + delta < 0 || index + delta >= copy.length){
|
||||
return;
|
||||
}
|
||||
|
@ -50,14 +48,14 @@ export default Ember.Controller.extend({
|
|||
item.set("editing", false);
|
||||
},
|
||||
add: function(){
|
||||
var obj = Em.Object.create({editing: true, name: "Enter Name"});
|
||||
const obj = this.store.createRecord('badge-grouping', {editing: true, name: I18n.t('admin.badges.badge_grouping')});
|
||||
this.get('workingCopy').pushObject(obj);
|
||||
},
|
||||
saveAll: function(){
|
||||
var self = this;
|
||||
const self = this;
|
||||
var items = this.get('workingCopy');
|
||||
var groupIds = items.map(function(i){return i.get("id") || -1;});
|
||||
var names = items.map(function(i){return i.get("name");});
|
||||
const groupIds = items.map(function(i){return i.get("id") || -1;});
|
||||
const names = items.map(function(i){return i.get("name");});
|
||||
|
||||
Discourse.ajax('/admin/badges/badge_groupings',{
|
||||
data: {ids: groupIds, names: names},
|
||||
|
@ -66,14 +64,13 @@ export default Ember.Controller.extend({
|
|||
items = self.get("model");
|
||||
items.clear();
|
||||
data.badge_groupings.forEach(function(g){
|
||||
items.pushObject(Em.Object.create(g));
|
||||
items.pushObject(self.store.createRecord('badge-grouping', g));
|
||||
});
|
||||
self.set('model', null);
|
||||
self.set('workingCopy', null);
|
||||
self.send('closeModal');
|
||||
},function(){
|
||||
// TODO we can do better
|
||||
bootbox.alert("Something went wrong");
|
||||
bootbox.alert(I18n.t('generic_error'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ const AdminUser = Discourse.User.extend({
|
|||
customGroups: Em.computed.filter("groups", (g) => !g.automatic && Group.create(g)),
|
||||
automaticGroups: Em.computed.filter("groups", (g) => g.automatic && Group.create(g)),
|
||||
|
||||
canViewProfile: Ember.computed.or("active", "staged"),
|
||||
|
||||
generateApiKey() {
|
||||
const self = this;
|
||||
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {
|
||||
|
@ -264,6 +266,7 @@ const AdminUser = Discourse.User.extend({
|
|||
},
|
||||
|
||||
unblock() {
|
||||
this.set('blockingUser', true);
|
||||
return Discourse.ajax('/admin/users/' + this.id + '/unblock', {
|
||||
type: 'PUT'
|
||||
}).then(function() {
|
||||
|
@ -275,14 +278,33 @@ const AdminUser = Discourse.User.extend({
|
|||
},
|
||||
|
||||
block() {
|
||||
return Discourse.ajax('/admin/users/' + this.id + '/block', {
|
||||
type: 'PUT'
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
bootbox.alert(error);
|
||||
});
|
||||
const user = this,
|
||||
message = I18n.t("admin.user.block_confirm");
|
||||
|
||||
const performBlock = function() {
|
||||
user.set('blockingUser', true);
|
||||
return Discourse.ajax('/admin/users/' + user.id + '/block', {
|
||||
type: 'PUT'
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
bootbox.alert(error);
|
||||
user.set('blockingUser', false);
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [{
|
||||
"label": I18n.t("composer.cancel"),
|
||||
"class": "cancel",
|
||||
"link": true
|
||||
}, {
|
||||
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.block_accept'),
|
||||
"class": "btn btn-danger",
|
||||
"callback": function() { performBlock(); }
|
||||
}];
|
||||
|
||||
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
|
||||
},
|
||||
|
||||
sendActivationEmail() {
|
||||
|
|
|
@ -4,7 +4,7 @@ const EmailLog = Discourse.Model.extend({});
|
|||
|
||||
EmailLog.reopenClass({
|
||||
|
||||
create: function(attrs) {
|
||||
create(attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (attrs.user) {
|
||||
|
@ -14,16 +14,15 @@ EmailLog.reopenClass({
|
|||
return this._super(attrs);
|
||||
},
|
||||
|
||||
findAll: function(filter) {
|
||||
findAll(filter, offset) {
|
||||
filter = filter || {};
|
||||
var status = filter.status || "all";
|
||||
offset = offset || 0;
|
||||
|
||||
const status = filter.status || "sent";
|
||||
filter = _.omit(filter, "status");
|
||||
|
||||
return Discourse.ajax("/admin/email/" + status + ".json", { data: filter }).then(function(logs) {
|
||||
return _.map(logs, function (log) {
|
||||
return EmailLog.create(log);
|
||||
});
|
||||
});
|
||||
return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
|
||||
.then(logs => _.map(logs, log => EmailLog.create(log)));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
29
app/assets/javascripts/admin/models/incoming-email.js.es6
Normal file
29
app/assets/javascripts/admin/models/incoming-email.js.es6
Normal file
|
@ -0,0 +1,29 @@
|
|||
import AdminUser from 'admin/models/admin-user';
|
||||
|
||||
const IncomingEmail = Discourse.Model.extend({});
|
||||
|
||||
IncomingEmail.reopenClass({
|
||||
|
||||
create(attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (attrs.user) {
|
||||
attrs.user = AdminUser.create(attrs.user);
|
||||
}
|
||||
|
||||
return this._super(attrs);
|
||||
},
|
||||
|
||||
findAll(filter, offset) {
|
||||
filter = filter || {};
|
||||
offset = offset || 0;
|
||||
|
||||
const status = filter.status || "received";
|
||||
filter = _.omit(filter, "status");
|
||||
|
||||
return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
|
||||
.then(incomings => _.map(incomings, incoming => IncomingEmail.create(incoming)));
|
||||
}
|
||||
});
|
||||
|
||||
export default IncomingEmail;
|
|
@ -1,4 +1,5 @@
|
|||
import Badge from 'discourse/models/badge';
|
||||
import BadgeGrouping from 'discourse/models/badge-grouping';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
_json: null,
|
||||
|
@ -13,14 +14,19 @@ export default Discourse.Route.extend({
|
|||
|
||||
setupController: function(controller, model) {
|
||||
var json = this._json,
|
||||
triggers = [];
|
||||
triggers = [],
|
||||
badgeGroupings = [];
|
||||
|
||||
_.each(json.admin_badges.triggers,function(v,k){
|
||||
triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)});
|
||||
});
|
||||
|
||||
json.badge_groupings.forEach(function(badgeGroupingJson) {
|
||||
badgeGroupings.push(BadgeGrouping.create(badgeGroupingJson));
|
||||
});
|
||||
|
||||
controller.setProperties({
|
||||
badgeGroupings: json.badge_groupings,
|
||||
badgeGroupings: badgeGroupings,
|
||||
badgeTypes: json.badge_types,
|
||||
protectedSystemFields: json.admin_badges.protected_system_fields,
|
||||
badgeTriggers: triggers,
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
import AdminEmailLogs from 'admin/routes/admin-email-logs';
|
||||
export default AdminEmailLogs.extend({ status: "all" });
|
|
@ -0,0 +1,14 @@
|
|||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model() {
|
||||
return IncomingEmail.findAll({ status: this.get("status") });
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
controller.set("filter", { status: this.get("status") });
|
||||
}
|
||||
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
import EmailSettings from 'admin/models/email-settings';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
model: function() {
|
||||
model() {
|
||||
return EmailSettings.find();
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
renderTemplate() {
|
||||
this.render('admin/templates/email_index', { into: 'adminEmail' });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
/**
|
||||
Handles routes related to viewing email logs.
|
||||
|
||||
@class AdminEmailSentRoute
|
||||
@extends Discourse.Route
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model: function() {
|
||||
model() {
|
||||
return EmailLog.findAll({ status: this.get("status") });
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
// resets the filters
|
||||
controller.set("filter", { status: this.get("status") });
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
this.render("admin/templates/email_" + this.get("status"), { into: "adminEmail" });
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
|
||||
export default AdminEmailIncomings.extend({ status: "received" });
|
|
@ -0,0 +1,2 @@
|
|||
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
|
||||
export default AdminEmailIncomings.extend({ status: "rejected" });
|
|
@ -8,9 +8,10 @@ export default {
|
|||
});
|
||||
|
||||
this.resource('adminEmail', { path: '/email'}, function() {
|
||||
this.route('all');
|
||||
this.route('sent');
|
||||
this.route('skipped');
|
||||
this.route('received');
|
||||
this.route('rejected');
|
||||
this.route('previewDigest', { path: '/preview-digest' });
|
||||
});
|
||||
|
||||
|
|
|
@ -2,25 +2,22 @@
|
|||
<form class="form-horizontal">
|
||||
<div>
|
||||
<label for="name">{{i18n 'admin.badges.name'}}</label>
|
||||
{{input type="text" name="name" value=buffered.name}}
|
||||
{{#if readOnly}}
|
||||
{{input type="text" name="name" value=buffered.displayName disabled=true}}
|
||||
{{else}}
|
||||
{{input type="text" name="name" value=buffered.name}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if showDisplayName}}
|
||||
<div>
|
||||
<strong>{{i18n 'admin.badges.display_name'}}</strong>
|
||||
{{buffered.displayName}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div>
|
||||
<label for="name">{{i18n 'admin.badges.icon'}}</label>
|
||||
{{input type="text" name="name" value=buffered.icon}}
|
||||
<label for="icon">{{i18n 'admin.badges.icon'}}</label>
|
||||
{{input type="text" name="icon" value=buffered.icon}}
|
||||
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="name">{{i18n 'admin.badges.image'}}</label>
|
||||
{{input type="text" name="name" value=buffered.image}}
|
||||
<label for="image">{{i18n 'admin.badges.image'}}</label>
|
||||
{{input type="text" name="image" value=buffered.image}}
|
||||
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
|
||||
</div>
|
||||
|
||||
|
@ -40,7 +37,7 @@
|
|||
value=buffered.badge_grouping_id
|
||||
content=badgeGroupings
|
||||
optionValuePath="content.id"
|
||||
optionLabelPath="content.name"}}
|
||||
optionLabelPath="content.displayName"}}
|
||||
<button {{action "editGroupings"}} class='btn'>{{fa-icon 'pencil'}}</button>
|
||||
</div>
|
||||
|
||||
|
|
55
app/assets/javascripts/admin/templates/email-received.hbs
Normal file
55
app/assets/javascripts/admin/templates/email-received.hbs
Normal file
|
@ -0,0 +1,55 @@
|
|||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr class="filters">
|
||||
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
|
||||
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each email in model}}
|
||||
<tr>
|
||||
<td class="time">{{format-date email.created_at}}</td>
|
||||
<td class="username">
|
||||
<div>
|
||||
{{#if email.user}}
|
||||
{{#link-to 'adminUser' email.user}}
|
||||
{{avatar email.user imageSize="tiny"}}
|
||||
{{email.from_address}}
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="addresses">
|
||||
{{#each to in email.to_addresses}}
|
||||
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
|
||||
{{/each}}
|
||||
{{#each cc in email.cc_addresses}}
|
||||
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
|
||||
{{/each}}
|
||||
</td>
|
||||
<td>
|
||||
{{#if email.post_url}}
|
||||
<a href="{{email.post_url}}">{{email.subject}}</a>
|
||||
{{else}}
|
||||
{{email.subject}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="4">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
|
||||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
52
app/assets/javascripts/admin/templates/email-rejected.hbs
Normal file
52
app/assets/javascripts/admin/templates/email-rejected.hbs
Normal file
|
@ -0,0 +1,52 @@
|
|||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.error'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr class="filters">
|
||||
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
|
||||
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each email in model}}
|
||||
<tr>
|
||||
<td class="time">{{format-date email.created_at}}</td>
|
||||
<td class="username">
|
||||
<div>
|
||||
{{#if email.user}}
|
||||
{{#link-to 'adminUser' email.user}}
|
||||
{{avatar email.user imageSize="tiny"}}
|
||||
{{email.from_address}}
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="addresses">
|
||||
{{#each to in email.to_addresses}}
|
||||
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
|
||||
{{/each}}
|
||||
{{#each cc in email.cc_addresses}}
|
||||
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
|
||||
{{/each}}
|
||||
</td>
|
||||
<td>{{email.subject}}</td>
|
||||
<td class="error">{{email.error}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
|
||||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
|
@ -1,4 +1,4 @@
|
|||
<table class='table'>
|
||||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.sent_at'}}</th>
|
||||
|
@ -37,3 +37,5 @@
|
|||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
|
@ -1,4 +1,4 @@
|
|||
<table class='table'>
|
||||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
|
@ -37,3 +37,5 @@
|
|||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
|
@ -1,10 +1,11 @@
|
|||
{{#admin-nav}}
|
||||
{{nav-item route='adminEmail.index' label='admin.email.settings'}}
|
||||
{{nav-item route='adminEmail.all' label='admin.email.all'}}
|
||||
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.email.templates'}}
|
||||
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
|
||||
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
|
||||
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||
{{nav-item route='adminEmail.received' label='admin.email.received'}}
|
||||
{{nav-item route='adminEmail.rejected' label='admin.email.rejected'}}
|
||||
{{/admin-nav}}
|
||||
|
||||
<div class="admin-container">
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<table class='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
<th>{{i18n 'admin.email.user'}}</th>
|
||||
<th>{{i18n 'admin.email.to_address'}}</th>
|
||||
<th>{{i18n 'admin.email.email_type'}}</th>
|
||||
<th>{{i18n 'admin.email.skipped_reason'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr class="filters">
|
||||
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
|
||||
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each l in model}}
|
||||
<tr>
|
||||
<td>{{format-date l.created_at}}</td>
|
||||
<td>
|
||||
{{#if l.user}}
|
||||
{{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</td>
|
||||
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
|
||||
<td>{{l.email_type}}</td>
|
||||
<td>{{l.skipped_reason}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
|
||||
{{/each}}
|
||||
|
||||
</table>
|
|
@ -5,15 +5,15 @@
|
|||
<li>
|
||||
{{#if wc.editing}}
|
||||
{{input value=wc.name}}
|
||||
<button {{action "save" wc}}><i class="fa fa-check"></i></button>
|
||||
<button {{action "save" wc}} class="btn no-text">{{fa-icon 'check'}}</button>
|
||||
{{else}}
|
||||
{{wc.name}}
|
||||
{{wc.displayName}}
|
||||
{{/if}}
|
||||
<div class='actions'>
|
||||
<button {{action "edit" wc}}><i class="fa fa-pencil"></i></button>
|
||||
<button {{action "up" wc}}><i class="fa fa-toggle-up"></i></button>
|
||||
<button {{action "down" wc}}><i class="fa fa-toggle-down"></i></button>
|
||||
<button {{action "delete" wc}}><i class="fa fa-times"></i></button>
|
||||
<button {{action "edit" wc}} class="btn no-text" {{bind-attr disabled="wc.system"}}>{{fa-icon 'pencil'}}</button>
|
||||
<button {{action "up" wc}} class="btn no-text">{{fa-icon 'toggle-up'}}</button>
|
||||
<button {{action "down" wc}} class="btn no-text">{{fa-icon 'toggle-down'}}</button>
|
||||
<button {{action "delete" wc}} class="btn no-text btn-danger" {{bind-attr disabled="wc.system"}}>{{fa-icon 'times'}}</button>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<section class="details {{unless model.active 'not-activated'}}">
|
||||
|
||||
<div class='user-controls'>
|
||||
{{#if model.active}}
|
||||
{{#if model.canViewProfile}}
|
||||
{{#link-to 'user' model class="btn"}}
|
||||
{{fa-icon "user"}}
|
||||
{{i18n 'admin.user.show_public_profile'}}
|
||||
{{/link-to}}
|
||||
{{/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"}}
|
||||
|
@ -327,15 +329,29 @@
|
|||
<div class='field'>{{i18n 'admin.user.blocked'}}</div>
|
||||
<div class='value'>{{model.blocked}}</div>
|
||||
<div class='controls'>
|
||||
{{#if model.blocked}}
|
||||
<button class='btn' {{action "unblock" target="content"}}>
|
||||
{{fa-icon "thumbs-o-up"}}
|
||||
{{i18n 'admin.user.unblock'}}
|
||||
</button>
|
||||
{{i18n 'admin.user.block_explanation'}}
|
||||
{{/if}}
|
||||
{{#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>
|
||||
{{i18n 'admin.user.block_explanation'}}
|
||||
{{else}}
|
||||
<button class='btn' {{action "block" target="content"}}>
|
||||
{{fa-icon "ban"}}
|
||||
{{i18n 'admin.user.block'}}
|
||||
</button>
|
||||
{{i18n 'admin.user.block_explanation'}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="display-row">
|
||||
<div class='field'>{{i18n 'admin.user.staged'}}</div>
|
||||
<div class='value'>{{model.staged}}</div>
|
||||
<div class='controls'>{{i18n 'admin.user.stage_explanation'}}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class='details'>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import LoadMore from "discourse/mixins/load-more";
|
||||
|
||||
export default Ember.View.extend(LoadMore, {
|
||||
loading: false,
|
||||
eyelineSelector: ".email-list tr",
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
if (this.get("loading") || this.get("model.allLoaded")) { return; }
|
||||
this.set("loading", true);
|
||||
return this.get("controller").loadMore().then(() => this.set("loading", false));
|
||||
}
|
||||
}
|
||||
});
|
14
app/assets/javascripts/admin/views/admin-email-logs.js.es6
Normal file
14
app/assets/javascripts/admin/views/admin-email-logs.js.es6
Normal file
|
@ -0,0 +1,14 @@
|
|||
import LoadMore from "discourse/mixins/load-more";
|
||||
|
||||
export default Ember.View.extend(LoadMore, {
|
||||
loading: false,
|
||||
eyelineSelector: ".email-list tr",
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
if (this.get("loading") || this.get("model.allLoaded")) { return; }
|
||||
this.set("loading", true);
|
||||
return this.get("controller").loadMore().then(() => this.set("loading", false));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
|
||||
|
||||
export default AdminEmailIncomingsView.extend({
|
||||
templateName: "admin/templates/email-received"
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
|
||||
|
||||
export default AdminEmailIncomingsView.extend({
|
||||
templateName: "admin/templates/email-rejected"
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailLogsView from "admin/views/admin-email-logs";
|
||||
|
||||
export default AdminEmailLogsView.extend({
|
||||
templateName: "admin/templates/email-sent"
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailLogsView from "admin/views/admin-email-logs";
|
||||
|
||||
export default AdminEmailLogsView.extend({
|
||||
templateName: "admin/templates/email-skipped"
|
||||
});
|
|
@ -6,9 +6,9 @@ import PermissionType from 'discourse/models/permission-type';
|
|||
|
||||
export default ComboboxView.extend({
|
||||
classNames: ['combobox category-combobox'],
|
||||
overrideWidths: true,
|
||||
dataAttributes: ['id', 'description_text'],
|
||||
valueBinding: Ember.Binding.oneWay('source'),
|
||||
overrideWidths: true,
|
||||
castInteger: true,
|
||||
|
||||
@computed("scopedCategoryId", "categories")
|
||||
|
@ -22,7 +22,6 @@ export default ComboboxView.extend({
|
|||
return categories.filter(c => {
|
||||
if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
|
||||
if (c.get('isUncategorizedCategory')) { return false; }
|
||||
if (c.get('contains_messages')) { return false; }
|
||||
return c.get('permission') === PermissionType.FULL;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
|
||||
_initializeAutocomplete: function() {
|
||||
const self = this,
|
||||
template = this.container.lookup('template:category-group-autocomplete.raw'),
|
||||
regexp = new RegExp("href=['\"]" + Discourse.getURL('/c/') + "([^'\"]+)");
|
||||
regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
|
||||
|
||||
this.$('input').autocomplete({
|
||||
items: this.get('categories'),
|
||||
single: false,
|
||||
allowAny: false,
|
||||
dataSource(term){
|
||||
return Discourse.Category.list().filter(function(category){
|
||||
return Category.list().filter(function(category){
|
||||
const regex = new RegExp(term, "i");
|
||||
return category.get("name").match(regex) &&
|
||||
!_.contains(self.get('blacklist') || [], category) &&
|
||||
|
@ -22,7 +23,7 @@ export default Ember.Component.extend({
|
|||
onChangeItems(items) {
|
||||
const categories = _.map(items, function(link) {
|
||||
const slug = link.match(regexp)[1];
|
||||
return Discourse.Category.findSingleBySlug(slug);
|
||||
return Category.findSingleBySlug(slug);
|
||||
});
|
||||
Em.run.next(() => self.set("categories", categories));
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import userSearch from 'discourse/lib/user-search';
|
||||
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
||||
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['wmd-controls'],
|
||||
|
@ -111,13 +112,19 @@ export default Ember.Component.extend({
|
|||
$preview.scrollTop(desired + 50);
|
||||
},
|
||||
|
||||
_renderUnseen: function($preview, unseen) {
|
||||
fetchUnseenMentions($preview, unseen, this.siteSettings).then(() => {
|
||||
_renderUnseenMentions: function($preview, unseen) {
|
||||
fetchUnseenMentions($preview, unseen).then(() => {
|
||||
linkSeenMentions($preview, this.siteSettings);
|
||||
this._warnMentionedGroups($preview);
|
||||
});
|
||||
},
|
||||
|
||||
_renderUnseenCategoryHashtags: function($preview, unseen) {
|
||||
fetchUnseenCategoryHashtags(unseen).then(() => {
|
||||
linkSeenCategoryHashtags($preview);
|
||||
});
|
||||
},
|
||||
|
||||
_warnMentionedGroups($preview) {
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
this._warnedMentions = this._warnedMentions || [];
|
||||
|
@ -386,11 +393,17 @@ export default Ember.Component.extend({
|
|||
// Paint mentions
|
||||
const unseen = linkSeenMentions($preview, this.siteSettings);
|
||||
if (unseen.length) {
|
||||
Ember.run.debounce(this, this._renderUnseen, $preview, unseen, 500);
|
||||
Ember.run.debounce(this, this._renderUnseenMentions, $preview, unseen, 500);
|
||||
}
|
||||
|
||||
this._warnMentionedGroups($preview);
|
||||
|
||||
// Paint category hashtags
|
||||
const unseenHashtags = linkSeenCategoryHashtags($preview);
|
||||
if (unseenHashtags.length) {
|
||||
Ember.run.debounce(this, this._renderUnseenCategoryHashtags, $preview, unseenHashtags, 500);
|
||||
}
|
||||
|
||||
const post = this.get('composer.post');
|
||||
let refresh = false;
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import loadScript from 'discourse/lib/load-script';
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
|
||||
import Category from 'discourse/models/category';
|
||||
import { SEPARATOR as categoryHashtagSeparator } from 'discourse/lib/category-hashtags';
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
|
@ -41,7 +43,7 @@ function Toolbar() {
|
|||
id: 'italic',
|
||||
group: 'fontStyles',
|
||||
shortcut: 'I',
|
||||
perform: e => e.applySurround('*', '*', 'italic_text')
|
||||
perform: e => e.applySurround('_', '_', 'italic_text')
|
||||
});
|
||||
|
||||
this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
|
||||
|
@ -175,7 +177,11 @@ export default Ember.Component.extend({
|
|||
|
||||
@on('didInsertElement')
|
||||
_startUp() {
|
||||
this._applyEmojiAutocomplete();
|
||||
const container = this.get('container'),
|
||||
$editorInput = this.$('.d-editor-input');
|
||||
|
||||
this._applyEmojiAutocomplete(container, $editorInput);
|
||||
this._applyCategoryHashtagAutocomplete(container, $editorInput);
|
||||
|
||||
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
|
||||
|
||||
|
@ -243,14 +249,49 @@ export default Ember.Component.extend({
|
|||
Ember.run.debounce(this, this._updatePreview, 30);
|
||||
},
|
||||
|
||||
_applyEmojiAutocomplete() {
|
||||
_applyCategoryHashtagAutocomplete(container, $editorInput) {
|
||||
const template = container.lookup('template:category-group-autocomplete.raw');
|
||||
|
||||
$editorInput.autocomplete({
|
||||
template: template,
|
||||
key: '#',
|
||||
transformComplete(category) {
|
||||
return Category.slugFor(category, categoryHashtagSeparator);
|
||||
},
|
||||
dataSource(term) {
|
||||
return Category.search(term);
|
||||
},
|
||||
triggerRule(textarea, opts) {
|
||||
const result = Discourse.Utilities.caretRowCol(textarea);
|
||||
const row = result.rowNum;
|
||||
var col = result.colNum;
|
||||
var line = textarea.value.split("\n")[row - 1];
|
||||
|
||||
if (opts && opts.backSpace) {
|
||||
col = col - 1;
|
||||
line = line.slice(0, line.length - 1);
|
||||
|
||||
// Don't trigger autocomplete when backspacing into a `#category |` => `#category|`
|
||||
if (/^#{1}\w+/.test(line)) return false;
|
||||
}
|
||||
|
||||
if (col < 6) {
|
||||
// Don't trigger autocomplete when ATX-style headers are used
|
||||
return (line.slice(0, col) !== "#".repeat(col));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_applyEmojiAutocomplete(container, $editorInput) {
|
||||
if (!this.siteSettings.enable_emoji) { return; }
|
||||
|
||||
const container = this.container;
|
||||
const template = container.lookup('template:emoji-selector-autocomplete.raw');
|
||||
const self = this;
|
||||
|
||||
this.$('.d-editor-input').autocomplete({
|
||||
$editorInput.autocomplete({
|
||||
template: template,
|
||||
key: ":",
|
||||
|
||||
|
|
|
@ -3,22 +3,24 @@ import loadScript from "discourse/lib/load-script";
|
|||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Em.Component.extend({
|
||||
tagName: "input",
|
||||
classNames: ["date-picker"],
|
||||
classNames: ["date-picker-wrapper"],
|
||||
_picker: null,
|
||||
|
||||
@on("didInsertElement")
|
||||
_loadDatePicker() {
|
||||
const input = this.$()[0];
|
||||
const input = this.$(".date-picker")[0];
|
||||
|
||||
loadScript("/javascripts/pikaday.js").then(() => {
|
||||
this._picker = new Pikaday({
|
||||
let default_opts = {
|
||||
field: input,
|
||||
container: this.$()[0],
|
||||
format: "YYYY-MM-DD",
|
||||
defaultDate: moment().add(1, "day").toDate(),
|
||||
minDate: new Date(),
|
||||
onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD")),
|
||||
});
|
||||
onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD"))
|
||||
};
|
||||
|
||||
this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -27,4 +29,8 @@ export default Em.Component.extend({
|
|||
this._picker = null;
|
||||
},
|
||||
|
||||
_opts() {
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -4,10 +4,15 @@ import { setting } from 'discourse/lib/computed';
|
|||
export default Ember.Component.extend({
|
||||
classNames: ["title"],
|
||||
|
||||
linkUrl: function() {
|
||||
return Discourse.getURL('/');
|
||||
targetUrl: function() {
|
||||
// For overriding by customizations
|
||||
return '/';
|
||||
}.property(),
|
||||
|
||||
linkUrl: function() {
|
||||
return Discourse.getURL(this.get('targetUrl'));
|
||||
}.property('targetUrl'),
|
||||
|
||||
showSmallLogo: function() {
|
||||
return !Discourse.Mobile.mobileView && this.get("minimized");
|
||||
}.property("minimized"),
|
||||
|
@ -27,7 +32,7 @@ export default Ember.Component.extend({
|
|||
|
||||
e.preventDefault();
|
||||
|
||||
DiscourseURL.routeTo('/');
|
||||
DiscourseURL.routeTo(this.get('targetUrl'));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -29,7 +29,9 @@ export default Ember.Component.extend({
|
|||
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
|
||||
}
|
||||
|
||||
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug);
|
||||
var username = it.get('data.username');
|
||||
username = username ? "?username=" + username.toLowerCase() : "";
|
||||
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username);
|
||||
}
|
||||
|
||||
const topicId = it.get('topic_id');
|
||||
|
|
|
@ -349,6 +349,21 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
|
|||
this.sendAction('toggleBookmark', post);
|
||||
},
|
||||
|
||||
// Wiki button
|
||||
buttonForWiki(post) {
|
||||
if (!post.get('can_wiki')) return;
|
||||
|
||||
if (post.get('wiki')) {
|
||||
return new Button('wiki', 'post.controls.unwiki', 'pencil-square-o', {className: 'wiki wikied'});
|
||||
} else {
|
||||
return new Button('wiki', 'post.controls.wiki', 'pencil-square-o', {className: 'wiki'});
|
||||
}
|
||||
},
|
||||
|
||||
clickWiki(post) {
|
||||
this.sendAction('toggleWiki', post);
|
||||
},
|
||||
|
||||
buttonForAdmin() {
|
||||
if (!Discourse.User.currentProp('canManageTopic')) { return; }
|
||||
return new Button('admin', 'post.controls.admin', 'wrench');
|
||||
|
@ -357,10 +372,7 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
|
|||
renderAdminPopup(post, buffer) {
|
||||
if (!Discourse.User.currentProp('canManageTopic')) { return; }
|
||||
|
||||
const isWiki = post.get('wiki'),
|
||||
wikiIcon = iconHTML('pencil-square-o'),
|
||||
wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki'),
|
||||
isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
|
||||
const isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
|
||||
postTypeIcon = iconHTML('shield'),
|
||||
postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'),
|
||||
rebakePostIcon = iconHTML('cog'),
|
||||
|
@ -373,7 +385,6 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
|
|||
const html = '<div class="post-admin-menu popup-menu">' +
|
||||
'<h3>' + I18n.t('admin_title') + '</h3>' +
|
||||
'<ul>' +
|
||||
'<li class="btn" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
|
||||
(Discourse.User.currentProp('staff') ? '<li class="btn" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
|
||||
'<li class="btn" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
|
||||
(post.hidden ? '<li class="btn" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
|
||||
|
@ -393,10 +404,6 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
|
|||
});
|
||||
},
|
||||
|
||||
clickToggleWiki() {
|
||||
this.sendAction('toggleWiki', this.get('post'));
|
||||
},
|
||||
|
||||
clickTogglePostType() {
|
||||
this.sendAction("togglePostType", this.get("post"));
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { relativeAge } from 'discourse/lib/formatter';
|
||||
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const icons = {
|
||||
'closed.enabled': 'lock',
|
||||
|
@ -13,16 +14,20 @@ const icons = {
|
|||
'pinned_globally.disabled': 'thumb-tack unpinned',
|
||||
'visible.enabled': 'eye',
|
||||
'visible.disabled': 'eye-slash',
|
||||
'split_topic': 'sign-out'
|
||||
'split_topic': 'sign-out',
|
||||
'invited_user': 'plus-circle',
|
||||
'removed_user': 'minus-circle'
|
||||
};
|
||||
|
||||
export function actionDescription(actionCode, createdAt) {
|
||||
export function actionDescription(actionCode, createdAt, username) {
|
||||
return function() {
|
||||
const ac = this.get(actionCode);
|
||||
if (ac) {
|
||||
const dt = new Date(this.get(createdAt));
|
||||
const when = relativeAge(dt, {format: 'medium-with-ago'});
|
||||
return I18n.t(`action_codes.${ac}`, {when}).htmlSafe();
|
||||
const when = autoUpdatingRelativeAge(dt, { format: 'medium-with-ago' });
|
||||
const u = this.get(username);
|
||||
const who = u ? `<a class="mention" href="/users/${u}">@${u}</a>` : "";
|
||||
return I18n.t(`action_codes.${ac}`, { who, when }).htmlSafe();
|
||||
}
|
||||
}.property(actionCode, createdAt);
|
||||
}
|
||||
|
@ -31,18 +36,19 @@ export default Ember.Component.extend({
|
|||
layoutName: 'components/small-action', // needed because `time-gap` inherits from this
|
||||
classNames: ['small-action'],
|
||||
|
||||
description: actionDescription('actionCode', 'post.created_at'),
|
||||
description: actionDescription('actionCode', 'post.created_at', 'post.action_code_who'),
|
||||
|
||||
icon: function() {
|
||||
return icons[this.get('actionCode')] || 'exclamation';
|
||||
}.property('actionCode'),
|
||||
@computed("actionCode")
|
||||
icon(actionCode) {
|
||||
return icons[actionCode] || 'exclamation';
|
||||
},
|
||||
|
||||
actions: {
|
||||
edit: function() {
|
||||
edit() {
|
||||
this.sendAction('editPost', this.get('post'));
|
||||
},
|
||||
|
||||
delete: function() {
|
||||
delete() {
|
||||
this.sendAction('deletePost', this.get('post'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { actionDescription } from "discourse/components/small-action";
|
|||
export default Ember.Component.extend({
|
||||
classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"],
|
||||
moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"),
|
||||
actionDescription: actionDescription("item.action_code", "item.created_at"),
|
||||
actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"),
|
||||
|
||||
actions: {
|
||||
removeBookmark(userAction) {
|
||||
|
|
|
@ -3,5 +3,13 @@ export default Ember.Component.extend({
|
|||
|
||||
showGrantCount: function() {
|
||||
return this.get('count') && this.get('count') > 1;
|
||||
}.property('count')
|
||||
}.property('count'),
|
||||
|
||||
badgeUrl: function(){
|
||||
// NOTE: I tried using a link-to helper here but the queryParams mean it fails
|
||||
var username = this.get('user.username_lower') || '';
|
||||
username = username !== '' ? "?username=" + username : '';
|
||||
return this.get('badge.url') + username;
|
||||
}.property("badge", "user")
|
||||
|
||||
});
|
||||
|
|
|
@ -1,17 +1,33 @@
|
|||
import UserBadge from 'discourse/models/user-badge';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ['username'],
|
||||
noMoreBadges: false,
|
||||
userBadges: null,
|
||||
needs: ["application"],
|
||||
|
||||
user: function(){
|
||||
if (this.get("username")) {
|
||||
return this.get('userBadges')[0].get('user');
|
||||
}
|
||||
}.property("username"),
|
||||
|
||||
grantCount: function() {
|
||||
if (this.get("username")) {
|
||||
return this.get('userBadges.grant_count');
|
||||
} else {
|
||||
return this.get('model.grant_count');
|
||||
}
|
||||
}.property('username', 'model', 'userBadges'),
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
const self = this;
|
||||
const userBadges = this.get('userBadges');
|
||||
|
||||
UserBadge.findByBadgeId(this.get('model.id'), {
|
||||
offset: userBadges.length
|
||||
offset: userBadges.length,
|
||||
username: this.get('username'),
|
||||
}).then(function(result) {
|
||||
userBadges.pushObjects(result);
|
||||
if(userBadges.length === 0){
|
||||
|
@ -22,11 +38,12 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
layoutClass: function(){
|
||||
var user = this.get("user") ? " single-user" : "";
|
||||
var ub = this.get("userBadges");
|
||||
if(ub && ub[0] && ub[0].post_id){
|
||||
return "user-badge-with-posts";
|
||||
return "user-badge-with-posts" + user;
|
||||
} else {
|
||||
return "user-badge-no-posts";
|
||||
return "user-badge-no-posts" + user;
|
||||
}
|
||||
}.property("userBadges"),
|
||||
|
||||
|
@ -34,7 +51,7 @@ export default Ember.Controller.extend({
|
|||
if (this.get('noMoreBadges')) { return false; }
|
||||
|
||||
if (this.get('userBadges')) {
|
||||
return this.get('model.grant_count') > this.get('userBadges.length');
|
||||
return this.get('grantCount') > this.get('userBadges.length');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
// You need a value in the field to submit it.
|
||||
submitDisabled: function() {
|
||||
return Ember.isEmpty(this.get('accountEmailOrUsername').trim()) || this.get('disabled');
|
||||
return Ember.isEmpty((this.get('accountEmailOrUsername') || '').trim()) || this.get('disabled');
|
||||
}.property('accountEmailOrUsername', 'disabled'),
|
||||
|
||||
onShow: function() {
|
||||
if ($.cookie('email')) {
|
||||
this.set('accountEmailOrUsername', $.cookie('email'));
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
submit: function() {
|
||||
var self = this;
|
||||
|
|
|
@ -6,6 +6,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
// If this isn't defined, it will proxy to the user model on the preferences
|
||||
// page which is wrong.
|
||||
emailOrUsername: null,
|
||||
inviteIcon: "envelope",
|
||||
|
||||
isAdmin: function(){
|
||||
return Discourse.User.currentProp("admin");
|
||||
|
@ -88,8 +89,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
if (Ember.isEmpty(this.get('emailOrUsername'))) {
|
||||
return I18n.t('topic.invite_reply.to_topic_blank');
|
||||
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
|
||||
this.set("inviteIcon", "envelope");
|
||||
return I18n.t('topic.invite_reply.to_topic_email');
|
||||
} else {
|
||||
this.set("inviteIcon", "hand-o-right");
|
||||
return I18n.t('topic.invite_reply.to_topic_username');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
}
|
||||
},
|
||||
|
||||
cannotDeleteAccount: Em.computed.not('can_delete_account'),
|
||||
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
|
||||
cannotDeleteAccount: Em.computed.not('currentUser.can_delete_account'),
|
||||
deleteDisabled: Em.computed.or('model.isSaving', 'deleting', 'cannotDeleteAccount'),
|
||||
|
||||
canEditName: setting('enable_names'),
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ export default Ember.Controller.extend({
|
|||
|
||||
newEmailEmpty: Em.computed.empty('newEmail'),
|
||||
saveDisabled: Em.computed.or('saving', 'newEmailEmpty', 'taken', 'unchanged'),
|
||||
unchanged: propertyEqual('newEmailLower', 'email'),
|
||||
unchanged: propertyEqual('newEmailLower', 'currentUser.email'),
|
||||
|
||||
newEmailLower: function() {
|
||||
return this.get('newEmail').toLowerCase();
|
||||
return this.get('newEmail').toLowerCase().trim();
|
||||
}.property('newEmail'),
|
||||
|
||||
saveButtonText: function() {
|
||||
|
@ -26,10 +26,10 @@ export default Ember.Controller.extend({
|
|||
this.set('saving', true);
|
||||
return this.get('content').changeEmail(this.get('newEmail')).then(function() {
|
||||
self.set('success', true);
|
||||
}, function(data) {
|
||||
}, function(e) {
|
||||
self.setProperties({ error: true, saving: false });
|
||||
if (data.responseJSON && data.responseJSON.errors && data.responseJSON.errors[0]) {
|
||||
self.set('errorMessage', data.responseJSON.errors[0]);
|
||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors && e.jqXHR.responseJSON.errors[0]) {
|
||||
self.set('errorMessage', e.jqXHR.responseJSON.errors[0]);
|
||||
} else {
|
||||
self.set('errorMessage', I18n.t('user.change_email.error'));
|
||||
}
|
||||
|
@ -38,5 +38,3 @@ export default Ember.Controller.extend({
|
|||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import Quote from 'discourse/lib/quote';
|
|||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
||||
|
@ -17,8 +18,8 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
queryParams: ['filter', 'username_filters', 'show_deleted'],
|
||||
loadedAllPosts: Em.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
|
||||
enteredAt: null,
|
||||
firstPostExpanded: false,
|
||||
retrying: false,
|
||||
firstPostExpanded: false,
|
||||
adminMenuVisible: false,
|
||||
|
||||
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
||||
|
@ -89,11 +90,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
this.set('selectedReplies', []);
|
||||
}.on('init'),
|
||||
|
||||
@computed("model.isPrivateMessage", "model.category_id")
|
||||
showCategoryChooser(isPrivateMessage, categoryId) {
|
||||
const category = Discourse.Category.findById(categoryId);
|
||||
const containsMessages = category && category.get("contains_messages");
|
||||
return !isPrivateMessage && !containsMessages;
|
||||
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
|
||||
|
||||
gotoInbox(name) {
|
||||
var url = '/users/' + this.get('currentUser.username_lower') + '/messages';
|
||||
if (name) {
|
||||
url = url + '/group/' + name;
|
||||
}
|
||||
DiscourseURL.routeTo(url);
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
@ -109,12 +113,19 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
this.deleteTopic();
|
||||
},
|
||||
|
||||
|
||||
archiveMessage() {
|
||||
this.get('model').archiveMessage();
|
||||
const topic = this.get('model');
|
||||
topic.archiveMessage().then(()=>{
|
||||
this.gotoInbox(topic.get("inboxGroupName"));
|
||||
});
|
||||
},
|
||||
|
||||
moveToInbox() {
|
||||
this.get('model').moveToInbox();
|
||||
const topic = this.get('model');
|
||||
topic.moveToInbox().then(()=>{
|
||||
this.gotoInbox(topic.get("inboxGroupName"));
|
||||
});
|
||||
},
|
||||
|
||||
// Post related methods
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { exportUserArchive } from 'discourse/lib/export-csv';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
userActionType: null,
|
||||
needs: ["application", "user"],
|
||||
|
@ -14,6 +16,21 @@ export default Ember.Controller.extend({
|
|||
showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded");
|
||||
}
|
||||
this.set("controllers.application.showFooter", showFooter);
|
||||
}.observes("userActionType", "model.stream.itemsLoaded")
|
||||
}.observes("userActionType", "model.stream.itemsLoaded"),
|
||||
|
||||
actions: {
|
||||
exportUserArchive() {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.export_csv.user_archive_confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
exportUserArchive();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export default Ember.ArrayController.extend({
|
||||
needs: ["user"],
|
||||
user: Em.computed.alias("controllers.user.model"),
|
||||
sortProperties: ['badge.badge_type.sort_order', 'badge.name'],
|
||||
orderBy: function(ub1, ub2){
|
||||
var sr1 = ub1.get('badge.badge_type.sort_order');
|
||||
|
|
|
@ -3,14 +3,23 @@ import Topic from 'discourse/models/topic';
|
|||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
needs: ["user-topics-list"],
|
||||
needs: ["user-topics-list", "user"],
|
||||
pmView: false,
|
||||
|
||||
viewingSelf: Em.computed.alias("controllers.user.viewingSelf"),
|
||||
isGroup: Em.computed.equal('pmView', 'groups'),
|
||||
|
||||
selected: Em.computed.alias('controllers.user-topics-list.selected'),
|
||||
bulkSelectEnabled: Em.computed.alias('controllers.user-topics-list.bulkSelectEnabled'),
|
||||
|
||||
mobileView: function() {
|
||||
return Discourse.Mobile.mobileView;
|
||||
}.property(),
|
||||
|
||||
showNewPM: function(){
|
||||
return this.get('controllers.user.viewingSelf') &&
|
||||
Discourse.User.currentProp('can_send_private_messages');
|
||||
}.property('controllers.user.viewingSelf'),
|
||||
|
||||
@computed('selected.@each', 'bulkSelectEnabled')
|
||||
hasSelection(selected, bulkSelectEnabled){
|
||||
return bulkSelectEnabled && selected && selected.length > 0;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export default Ember.Controller.extend({
|
||||
needs: ['user'],
|
||||
user: Em.computed.alias('controllers.user.model'),
|
||||
moreTopics: function(){
|
||||
return this.get('model.topics').length > 5;
|
||||
}.property('model'),
|
||||
moreReplies: function(){
|
||||
return this.get('model.replies').length > 5;
|
||||
}.property('model'),
|
||||
moreBadges: function(){
|
||||
return this.get('model.badges').length > 5;
|
||||
}.property('model')
|
||||
});
|
|
@ -14,9 +14,4 @@ export default Ember.Controller.extend({
|
|||
}
|
||||
},
|
||||
|
||||
showNewPM: function(){
|
||||
return this.get('controllers.user.viewingSelf') &&
|
||||
Discourse.User.currentProp('can_send_private_messages');
|
||||
}.property('controllers.user.viewingSelf')
|
||||
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { exportUserArchive } from 'discourse/lib/export-csv';
|
||||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import UserAction from 'discourse/models/user-action';
|
||||
|
@ -89,17 +88,5 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
.then(user => user.destroy({deletePosts: true}));
|
||||
},
|
||||
|
||||
exportUserArchive() {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.export_csv.user_archive_confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
exportUserArchive();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
Supports Discourse's category hashtags (#category-slug) for automatically
|
||||
generating a link to the category.
|
||||
**/
|
||||
Discourse.Dialect.inlineRegexp({
|
||||
start: '#',
|
||||
matcher: /^#([\w-:]{1,50})/i,
|
||||
spaceOrTagBoundary: true,
|
||||
|
||||
emitter: function(matches) {
|
||||
var slug = matches[1],
|
||||
hashtag = matches[0],
|
||||
attributeClass = 'hashtag',
|
||||
categoryHashtagLookup = this.dialect.options.categoryHashtagLookup,
|
||||
result = categoryHashtagLookup && categoryHashtagLookup(slug);
|
||||
|
||||
if (result && result[0] === "category") {
|
||||
return ['a', { class: attributeClass, href: result[1] }, '#', ["span", {}, slug]];
|
||||
} else {
|
||||
return ['span', { class: attributeClass }, hashtag];
|
||||
}
|
||||
}
|
||||
});
|
|
@ -5,22 +5,23 @@
|
|||
**/
|
||||
Discourse.Dialect.inlineRegexp({
|
||||
start: '@',
|
||||
// NOTE: we really should be using SiteSettings here, but it loads later in process
|
||||
// also, if we do, we must ensure serverside version works as well
|
||||
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9\_])/,
|
||||
// NOTE: since we can't use SiteSettings here (they loads later in process)
|
||||
// we are being less strict to account for more cases than allowed
|
||||
matcher: /^@(\w[\w.-]{0,59})\b/i,
|
||||
wordBoundary: true,
|
||||
|
||||
emitter: function(matches) {
|
||||
var username = matches[1],
|
||||
var mention = matches[0].trim(),
|
||||
name = matches[1],
|
||||
mentionLookup = this.dialect.options.mentionLookup;
|
||||
|
||||
var type = mentionLookup && mentionLookup(username.substr(1));
|
||||
var type = mentionLookup && mentionLookup(name);
|
||||
if (type === "user") {
|
||||
return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username];
|
||||
return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + name.toLowerCase()}, mention];
|
||||
} else if (type === "group") {
|
||||
return ['a', {'class': 'mention-group', href: Discourse.getURL("/groups/") + username.substr(1)}, username];
|
||||
return ['a', {'class': 'mention-group', href: Discourse.getURL("/groups/") + name}, mention];
|
||||
} else {
|
||||
return ['span', {'class': 'mention'}, username];
|
||||
return ['span', {'class': 'mention'}, mention];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -34,10 +35,10 @@ Discourse.Dialect.on("parseNode", function(event) {
|
|||
var parent = path[path.length - 1];
|
||||
// If the parent is an 'a', remove it
|
||||
if (parent && parent[0] === 'a') {
|
||||
var username = node[2];
|
||||
var name = node[2];
|
||||
node.length = 0;
|
||||
node[0] = "__RAW";
|
||||
node[1] = username;
|
||||
node[1] = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,16 @@ function categoryStripe(color, classes) {
|
|||
return "<span class='" + classes + "' " + style + "></span>";
|
||||
}
|
||||
|
||||
/**
|
||||
Generates category badge HTML
|
||||
|
||||
@param {Object} category The category to generate the badge for.
|
||||
@param {Object} opts
|
||||
@param {String} [opts.url] The url that we want the category badge to link to.
|
||||
@param {Boolean} [opts.allowUncategorized] If false, returns an empty string for the uncategorized category.
|
||||
@param {Boolean} [opts.link] If false, the category badge will not be a link.
|
||||
@param {Boolean} [opts.hideParaent] If true, parent category will be hidden in the badge.
|
||||
**/
|
||||
export function categoryBadgeHTML(category, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
|
@ -21,7 +31,7 @@ export function categoryBadgeHTML(category, opts) {
|
|||
|
||||
var description = get(category, 'description_text'),
|
||||
restricted = get(category, 'read_restricted'),
|
||||
url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
|
||||
url = opts.url ? opts.url : Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
|
||||
href = (opts.link === false ? '' : url),
|
||||
tagName = (opts.link === false || opts.link === "false" ? 'span' : 'a'),
|
||||
extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''),
|
||||
|
@ -50,6 +60,7 @@ export function categoryBadgeHTML(category, opts) {
|
|||
">";
|
||||
|
||||
var name = escapeExpression(get(category, 'name'));
|
||||
|
||||
if (restricted) {
|
||||
html += iconHTML('lock') + " " + name;
|
||||
} else {
|
||||
|
|
|
@ -24,6 +24,5 @@ registerUnbound('raw', function(templateName, params) {
|
|||
Ember.warn('Could not find raw template: ' + templateName);
|
||||
return;
|
||||
}
|
||||
|
||||
return renderRaw(this, template, templateName, params);
|
||||
});
|
||||
|
|
|
@ -8,9 +8,9 @@ function resolveParams(ctx, options) {
|
|||
if (options.hashTypes) {
|
||||
Ember.keys(hash).forEach(function(k) {
|
||||
const type = options.hashTypes[k];
|
||||
if (type === "STRING") {
|
||||
if (type === "STRING" || type === "StringLiteral") {
|
||||
params[k] = hash[k];
|
||||
} else if (type === "ID") {
|
||||
} else if (type === "ID" || type === "PathExpression") {
|
||||
params[k] = get(ctx, hash[k], options);
|
||||
}
|
||||
});
|
||||
|
@ -23,7 +23,7 @@ function resolveParams(ctx, options) {
|
|||
|
||||
export default function registerUnbound(name, fn) {
|
||||
const func = function(property, options) {
|
||||
if (options.types && options.types[0] === "ID") {
|
||||
if (options.types && (options.types[0] === "ID" || options.types[0] === "PathExpression")) {
|
||||
property = get(this, property, options);
|
||||
}
|
||||
|
||||
|
|
|
@ -282,6 +282,14 @@ export default function(options) {
|
|||
}, 50);
|
||||
});
|
||||
|
||||
const checkTriggerRule = (opts) => {
|
||||
if (options.triggerRule) {
|
||||
return options.triggerRule(me[0], opts);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$(this).on('keypress.autocomplete', function(e) {
|
||||
var caretPosition, term;
|
||||
|
||||
|
@ -289,7 +297,7 @@ export default function(options) {
|
|||
if (options.key && e.which === options.key.charCodeAt(0)) {
|
||||
caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
var prevChar = me.val().charAt(caretPosition - 1);
|
||||
if (!prevChar || allowedLettersRegex.test(prevChar)) {
|
||||
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
|
||||
completeStart = completeEnd = caretPosition;
|
||||
updateAutoComplete(options.dataSource(""));
|
||||
}
|
||||
|
@ -343,7 +351,7 @@ export default function(options) {
|
|||
stopFound = prev === options.key;
|
||||
if (stopFound) {
|
||||
prev = me[0].value[c - 1];
|
||||
if (!prev || allowedLettersRegex.test(prev)) {
|
||||
if (checkTriggerRule({ backSpace: true }) && (!prev || allowedLettersRegex.test(prev))) {
|
||||
completeStart = c;
|
||||
caretPosition = completeEnd = initial;
|
||||
term = me[0].value.substring(c + 1, initial);
|
||||
|
@ -351,7 +359,7 @@ export default function(options) {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
prevIsGood = /[a-zA-Z\.]/.test(prev);
|
||||
prevIsGood = /[a-zA-Z\.-]/.test(prev);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export const SEPARATOR = ":";
|
||||
|
||||
export function replaceSpan($elem, categorySlug, categoryLink) {
|
||||
$elem.replaceWith(`<a href="${categoryLink}" class="hashtag">#<span>${categorySlug}</span></a>`);
|
||||
};
|
|
@ -1,5 +1,10 @@
|
|||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export function isValidLink($link) {
|
||||
return ($link.hasClass("track-link") ||
|
||||
$link.closest('.hashtag,.badge-category,.onebox-result,.onebox-body').length === 0);
|
||||
};
|
||||
|
||||
export default {
|
||||
trackClick(e) {
|
||||
// cancel click if triggered as part of selection.
|
||||
|
@ -32,8 +37,7 @@ export default {
|
|||
var $badge = $('span.badge', $link);
|
||||
if ($badge.length === 1) {
|
||||
// don't update counts in category badge nor in oneboxes (except when we force it)
|
||||
if ($link.hasClass("track-link") ||
|
||||
$link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
|
||||
if (isValidLink($link)) {
|
||||
var html = $badge.html();
|
||||
if (/^\d+$/.test(html)) { $badge.html(parseInt(html, 10) + 1); }
|
||||
}
|
||||
|
|
|
@ -68,19 +68,32 @@
|
|||
RawHandlebars.JavaScriptCompiler.prototype.compiler = RawHandlebars.JavaScriptCompiler;
|
||||
RawHandlebars.JavaScriptCompiler.prototype.namespace = "Discourse.EmberCompatHandlebars";
|
||||
|
||||
function replaceGet(ast) {
|
||||
var visitor = new Handlebars.Visitor();
|
||||
visitor.mutating = true;
|
||||
|
||||
RawHandlebars.Compiler.prototype.mustache = function(mustache) {
|
||||
if ( !(mustache.params.length || mustache.hash)) {
|
||||
|
||||
var id = new Handlebars.AST.IdNode([{ part: 'get' }]);
|
||||
mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, mustache.escaped);
|
||||
}
|
||||
|
||||
return Handlebars.Compiler.prototype.mustache.call(this, mustache);
|
||||
};
|
||||
visitor.MustacheStatement = function(mustache) {
|
||||
if (!(mustache.params.length || mustache.hash)) {
|
||||
mustache.params[0] = mustache.path;
|
||||
mustache.path = {
|
||||
type: "PathExpression",
|
||||
data: false,
|
||||
depth: mustache.path.depth,
|
||||
parts: ["get"],
|
||||
original: "get",
|
||||
loc: mustache.path.loc,
|
||||
strict: true,
|
||||
falsy: true
|
||||
};
|
||||
}
|
||||
return Handlebars.Visitor.prototype.MustacheStatement.call(this, mustache);
|
||||
};
|
||||
visitor.accept(ast);
|
||||
}
|
||||
|
||||
RawHandlebars.precompile = function(value, asObject) {
|
||||
var ast = Handlebars.parse(value);
|
||||
replaceGet(ast);
|
||||
|
||||
var options = {
|
||||
knownHelpers: {
|
||||
|
@ -96,9 +109,10 @@
|
|||
return new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject);
|
||||
};
|
||||
|
||||
|
||||
RawHandlebars.compile = function(string) {
|
||||
var ast = Handlebars.parse(string);
|
||||
replaceGet(ast);
|
||||
|
||||
// this forces us to rewrite helpers
|
||||
var options = { data: true, stringParams: true };
|
||||
var environment = new RawHandlebars.Compiler().compile(ast, options);
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { replaceSpan } from 'discourse/lib/category-hashtags';
|
||||
|
||||
const validCategoryHashtags = {};
|
||||
const checkedCategoryHashtags = [];
|
||||
const testedKey = 'tested';
|
||||
const testedClass = `hashtag-${testedKey}`;
|
||||
|
||||
function updateFound($hashtags, categorySlugs) {
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
$hashtags.each((index, hashtag) => {
|
||||
const categorySlug = categorySlugs[index];
|
||||
const link = validCategoryHashtags[categorySlug];
|
||||
const $hashtag = $(hashtag);
|
||||
|
||||
if (link) {
|
||||
replaceSpan($hashtag, categorySlug, link);
|
||||
} else if (checkedCategoryHashtags.indexOf(categorySlug) !== -1) {
|
||||
$hashtag.addClass(testedClass);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function linkSeenCategoryHashtags($elem) {
|
||||
const $hashtags = $(`span.hashtag:not(.${testedClass})`, $elem);
|
||||
const unseen = [];
|
||||
|
||||
if ($hashtags.length) {
|
||||
const categorySlugs = $hashtags.map((_, hashtag) => $(hashtag).text().substr(1));
|
||||
if (categorySlugs.length) {
|
||||
_.uniq(categorySlugs).forEach((categorySlug) => {
|
||||
if (checkedCategoryHashtags.indexOf(categorySlug) === -1) {
|
||||
unseen.push(categorySlug);
|
||||
}
|
||||
});
|
||||
}
|
||||
updateFound($hashtags, categorySlugs);
|
||||
}
|
||||
|
||||
return unseen;
|
||||
};
|
||||
|
||||
export function fetchUnseenCategoryHashtags(categorySlugs) {
|
||||
return Discourse.ajax("/category_hashtags/check", { data: { category_slugs: categorySlugs } })
|
||||
.then((response) => {
|
||||
response.valid.forEach((category) => {
|
||||
validCategoryHashtags[category.slug] = category.url;
|
||||
});
|
||||
checkedCategoryHashtags.push.apply(checkedCategoryHashtags, categorySlugs);
|
||||
});
|
||||
}
|
|
@ -239,6 +239,7 @@ Discourse.Markdown.whiteListTag('a', 'class', 'attachment');
|
|||
Discourse.Markdown.whiteListTag('a', 'class', 'onebox');
|
||||
Discourse.Markdown.whiteListTag('a', 'class', 'mention');
|
||||
Discourse.Markdown.whiteListTag('a', 'class', 'mention-group');
|
||||
Discourse.Markdown.whiteListTag('a', 'class', 'hashtag');
|
||||
|
||||
Discourse.Markdown.whiteListTag('a', 'target', '_blank');
|
||||
Discourse.Markdown.whiteListTag('a', 'rel', 'nofollow');
|
||||
|
@ -251,6 +252,7 @@ Discourse.Markdown.whiteListTag('div', 'class', 'title');
|
|||
Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls');
|
||||
|
||||
Discourse.Markdown.whiteListTag('span', 'class', 'mention');
|
||||
Discourse.Markdown.whiteListTag('span', 'class', 'hashtag');
|
||||
Discourse.Markdown.whiteListTag('aside', 'class', 'quote');
|
||||
Discourse.Markdown.whiteListTag('aside', 'data-*');
|
||||
|
||||
|
|
|
@ -143,6 +143,19 @@ Discourse.Utilities = {
|
|||
return String(text).trim();
|
||||
},
|
||||
|
||||
// Determine the row and col of the caret in an element
|
||||
caretRowCol: function(el) {
|
||||
var caretPosition = Discourse.Utilities.caretPosition(el);
|
||||
var rows = el.value.slice(0, caretPosition).split("\n");
|
||||
var rowNum = rows.length;
|
||||
|
||||
var colNum = caretPosition - rows.splice(0, rowNum - 1).reduce(function(sum, row) {
|
||||
return sum + row.length + 1;
|
||||
}, 0);
|
||||
|
||||
return { rowNum: rowNum, colNum: colNum};
|
||||
},
|
||||
|
||||
// Determine the position of the caret in an element
|
||||
caretPosition: function(el) {
|
||||
var r, rc, re;
|
||||
|
@ -249,7 +262,11 @@ Discourse.Utilities = {
|
|||
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)$/i).test(upload.original_filename)) {
|
||||
// is Audio/Video
|
||||
return "http://" + Discourse.BaseUrl + upload.url;
|
||||
if (Discourse.CDN) {
|
||||
return Discourse.CDN.startsWith('//') ? "http:" + Discourse.getURLWithCDN(upload.url) : Discourse.getURLWithCDN(upload.url);
|
||||
} else {
|
||||
return "http://" + Discourse.BaseUrl + upload.url;
|
||||
}
|
||||
} else {
|
||||
return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a> (' + I18n.toHumanSize(upload.filesize) + ')';
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export default RestModel.extend({
|
|||
return this.get('name').toLowerCase().replace(/\s/g, '_');
|
||||
},
|
||||
|
||||
@computed
|
||||
@computed('name')
|
||||
displayName() {
|
||||
const i18nKey = `badges.badge_grouping.${this.get('i18nNameKey')}.name`;
|
||||
return I18n.t(i18nKey, {defaultValue: this.get('name')});
|
||||
|
|
|
@ -5,6 +5,10 @@ const Badge = RestModel.extend({
|
|||
|
||||
newBadge: Em.computed.none('id'),
|
||||
|
||||
url: function() {
|
||||
return Discourse.getURL(`/badges/${this.get('id')}/${this.get('slug')}`);
|
||||
}.property(),
|
||||
|
||||
/**
|
||||
@private
|
||||
|
||||
|
@ -159,7 +163,7 @@ Badge.reopenClass({
|
|||
let badges = [];
|
||||
if ("badge" in json) {
|
||||
badges = [json.badge];
|
||||
} else {
|
||||
} else if (json.badges) {
|
||||
badges = json.badges;
|
||||
}
|
||||
badges = badges.map(function(badgeJson) {
|
||||
|
@ -207,4 +211,3 @@ Badge.reopenClass({
|
|||
});
|
||||
|
||||
export default Badge;
|
||||
|
||||
|
|
|
@ -86,8 +86,7 @@ const Category = RestModel.extend({
|
|||
allow_badges: this.get('allow_badges'),
|
||||
custom_fields: this.get('custom_fields'),
|
||||
topic_template: this.get('topic_template'),
|
||||
suppress_from_homepage: this.get('suppress_from_homepage'),
|
||||
contains_messages: this.get("contains_messages"),
|
||||
suppress_from_homepage: this.get('suppress_from_homepage')
|
||||
},
|
||||
type: this.get('id') ? 'PUT' : 'POST'
|
||||
});
|
||||
|
@ -205,14 +204,14 @@ Category.reopenClass({
|
|||
return _uncategorized;
|
||||
},
|
||||
|
||||
slugFor(category) {
|
||||
slugFor(category, separator = "/") {
|
||||
if (!category) return "";
|
||||
|
||||
const parentCategory = Em.get(category, 'parentCategory');
|
||||
let result = "";
|
||||
|
||||
if (parentCategory) {
|
||||
result = Category.slugFor(parentCategory) + "/";
|
||||
result = Category.slugFor(parentCategory) + separator;
|
||||
}
|
||||
|
||||
const id = Em.get(category, 'id'),
|
||||
|
@ -285,6 +284,64 @@ Category.reopenClass({
|
|||
|
||||
reloadById(id) {
|
||||
return Discourse.ajax(`/c/${id}/show.json`);
|
||||
},
|
||||
|
||||
search(term, opts) {
|
||||
var limit = 5;
|
||||
|
||||
if (opts) {
|
||||
if (opts.limit === 0) {
|
||||
return [];
|
||||
} else if (opts.limit) {
|
||||
limit = opts.limit;
|
||||
}
|
||||
}
|
||||
|
||||
const emptyTerm = (term === "");
|
||||
let slugTerm = term;
|
||||
|
||||
if (!emptyTerm) {
|
||||
term = term.toLowerCase();
|
||||
slugTerm = term;
|
||||
term = term.replace(/-/g, " ");
|
||||
}
|
||||
|
||||
const categories = Category.listByActivity();
|
||||
const length = categories.length;
|
||||
var i;
|
||||
var data = [];
|
||||
|
||||
const done = () => {
|
||||
return data.length === limit;
|
||||
};
|
||||
|
||||
for (i = 0; i < length && !done(); i++) {
|
||||
const category = categories[i];
|
||||
if ((emptyTerm && !category.get('parent_category_id')) ||
|
||||
(!emptyTerm &&
|
||||
(category.get('name').toLowerCase().indexOf(term) === 0 ||
|
||||
category.get('slug').toLowerCase().indexOf(slugTerm) === 0))) {
|
||||
|
||||
data.push(category);
|
||||
}
|
||||
}
|
||||
|
||||
if (!done()) {
|
||||
for (i = 0; i < length && !done(); i++) {
|
||||
const category = categories[i];
|
||||
|
||||
if (!emptyTerm &&
|
||||
(category.get('name').toLowerCase().indexOf(term) > 0 ||
|
||||
category.get('slug').toLowerCase().indexOf(slugTerm) > 0)) {
|
||||
|
||||
if (data.indexOf(category) === -1) data.push(category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.sortBy(data, (category) => {
|
||||
return category.get('read_restricted');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -67,17 +67,16 @@ const Composer = RestModel.extend({
|
|||
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
|
||||
notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'),
|
||||
|
||||
@computed("privateMessage", "archetype.hasOptions", "categoryId")
|
||||
showCategoryChooser(isPrivateMessage, hasOptions, categoryId) {
|
||||
@computed("privateMessage", "archetype.hasOptions")
|
||||
showCategoryChooser(isPrivateMessage, hasOptions) {
|
||||
const manyCategories = Discourse.Category.list().length > 1;
|
||||
const category = Discourse.Category.findById(categoryId);
|
||||
const containsMessages = category && category.get("contains_messages");
|
||||
return !isPrivateMessage && !containsMessages && (hasOptions || manyCategories);
|
||||
return !isPrivateMessage && (hasOptions || manyCategories);
|
||||
},
|
||||
|
||||
privateMessage: function(){
|
||||
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message';
|
||||
}.property('creatingPrivateMessage', 'topic'),
|
||||
@computed("creatingPrivateMessage", "topic")
|
||||
privateMessage(creatingPrivateMessage, topic) {
|
||||
return creatingPrivateMessage || (topic && topic.get('archetype') === 'private_message');
|
||||
},
|
||||
|
||||
topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'),
|
||||
|
||||
|
|
|
@ -424,8 +424,12 @@ const Topic = RestModel.extend({
|
|||
this.set("archiving", true);
|
||||
var promise = Discourse.ajax(`/t/${this.get('id')}/archive-message`, {type: 'PUT'});
|
||||
|
||||
promise.then(()=>this.set('message_archived', true))
|
||||
.finally(()=>this.set('archiving', false));
|
||||
promise.then((msg)=> {
|
||||
this.set('message_archived', true);
|
||||
if (msg && msg.group_name) {
|
||||
this.set('inboxGroupName', msg.group_name);
|
||||
}
|
||||
}).finally(()=>this.set('archiving', false));
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
@ -434,8 +438,12 @@ const Topic = RestModel.extend({
|
|||
this.set("archiving", true);
|
||||
var promise = Discourse.ajax(`/t/${this.get('id')}/move-to-inbox`, {type: 'PUT'});
|
||||
|
||||
promise.then(()=>this.set('message_archived', false))
|
||||
.finally(()=>this.set('archiving', false));
|
||||
promise.then((msg)=> {
|
||||
this.set('message_archived', false);
|
||||
if (msg && msg.group_name) {
|
||||
this.set('inboxGroupName', msg.group_name);
|
||||
}
|
||||
}).finally(()=>this.set('archiving', false));
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ UserBadge.reopenClass({
|
|||
if ("user_badge" in json) {
|
||||
userBadges = [json.user_badge];
|
||||
} else {
|
||||
userBadges = json.user_badges;
|
||||
userBadges = (json.user_badge_info && json.user_badge_info.user_badges) || json.user_badges;
|
||||
}
|
||||
|
||||
userBadges = userBadges.map(function(userBadgeJson) {
|
||||
|
@ -73,6 +73,10 @@ UserBadge.reopenClass({
|
|||
if ("user_badge" in json) {
|
||||
return userBadges[0];
|
||||
} else {
|
||||
if (json.user_badge_info) {
|
||||
userBadges.grant_count = json.user_badge_info.grant_count;
|
||||
userBadges.username = json.user_badge_info.username;
|
||||
}
|
||||
return userBadges;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import UserBadge from 'discourse/models/user-badge';
|
|||
import UserActionStat from 'discourse/models/user-action-stat';
|
||||
import UserAction from 'discourse/models/user-action';
|
||||
import Group from 'discourse/models/group';
|
||||
import Topic from 'discourse/models/topic';
|
||||
|
||||
const User = RestModel.extend({
|
||||
|
||||
|
@ -355,6 +356,38 @@ const User = RestModel.extend({
|
|||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
summary() {
|
||||
return Discourse.ajax(`/users/${this.get("username_lower")}/summary.json`)
|
||||
.then(json => {
|
||||
const topicMap = {};
|
||||
|
||||
json.topics.forEach(t => {
|
||||
topicMap[t.id] = Topic.create(t);
|
||||
});
|
||||
|
||||
const badgeMap = {};
|
||||
Badge.createFromJson(json).forEach(b => {
|
||||
badgeMap[b.id] = b;
|
||||
});
|
||||
const summary = json["user_summary"];
|
||||
|
||||
summary.replies.forEach(r => {
|
||||
r.topic = topicMap[r.topic_id];
|
||||
r.url = r.topic.urlForPostNumber(r.post_number);
|
||||
r.createdAt = new Date(r.created_at);
|
||||
});
|
||||
|
||||
summary.topics = summary.topic_ids.map(id => topicMap[id]);
|
||||
|
||||
summary.badges = summary.badges.map(ub => {
|
||||
const badge = badgeMap[ub.badge_id];
|
||||
badge.count = ub.count;
|
||||
return badge;
|
||||
});
|
||||
return summary;
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -42,6 +42,7 @@ export default function() {
|
|||
this.route('parentCategory', { path: '/c/:slug' });
|
||||
this.route('categoryNone', { path: '/c/:slug/none' });
|
||||
this.route('category', { path: '/c/:parentSlug/:slug' });
|
||||
this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' });
|
||||
|
||||
// homepage
|
||||
this.route(Discourse.Utilities.defaultHomepage(), { path: '/' });
|
||||
|
@ -57,11 +58,13 @@ export default function() {
|
|||
// User routes
|
||||
this.resource('users');
|
||||
this.resource('user', { path: '/users/:username' }, function() {
|
||||
this.route('summary');
|
||||
this.resource('userActivity', { path: '/activity' }, function() {
|
||||
this.route('topics');
|
||||
this.route('replies');
|
||||
this.route('likesGiven', {path: 'likes-given'});
|
||||
this.route('bookmarks');
|
||||
this.route('pending');
|
||||
});
|
||||
|
||||
this.resource('userNotifications', {path: '/notifications'}, function(){
|
||||
|
|
|
@ -2,6 +2,11 @@ import UserBadge from 'discourse/models/user-badge';
|
|||
import Badge from 'discourse/models/badge';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: {
|
||||
username: {
|
||||
refreshModel: true
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
didTransition() {
|
||||
this.controllerFor("badges/show")._showFooter();
|
||||
|
@ -24,10 +29,13 @@ export default Discourse.Route.extend({
|
|||
}
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
return UserBadge.findByBadgeId(model.get("id")).then(userBadges => {
|
||||
afterModel(model,transition) {
|
||||
const username = transition.queryParams && transition.queryParams.username;
|
||||
|
||||
return UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
|
||||
this.userBadges = userBadges;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
titleToken() {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import Category from 'discourse/models/category';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
model: function(params) {
|
||||
return Category.findById(params.id);
|
||||
},
|
||||
|
||||
redirect: function(model) {
|
||||
this.transitionTo(`/c/${Category.slugFor(model)}`);
|
||||
}
|
||||
});
|
|
@ -1,2 +0,0 @@
|
|||
export default Discourse.Route.extend({
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
return this.modelFor("User").summary();
|
||||
}
|
||||
});
|
|
@ -9,7 +9,9 @@
|
|||
<div class='row'>
|
||||
<div class='badge'>{{user-badge badge=model}}</div>
|
||||
<div class='description'>{{{model.displayDescriptionHtml}}}</div>
|
||||
<div class='grant-count'>{{i18n 'badges.granted' count=model.grant_count}}</div>
|
||||
{{#unless user}}
|
||||
<div class='grant-count'>{{i18n 'badges.granted' count=grantCount}}</div>
|
||||
{{/unless}}
|
||||
<div class='info'>{{i18n 'badges.allow_title'}} {{{view.allowTitle}}}<br>{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,23 +24,48 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if user}}
|
||||
<div class='badge-user-info'>
|
||||
{{#link-to 'user' user}}
|
||||
{{avatar user imageSize="extra_large"}}
|
||||
<div class="details clearfix">
|
||||
{{poster-name post=user}}
|
||||
</div>
|
||||
{{/link-to}}
|
||||
<div class='earned'>
|
||||
{{i18n 'badges.earned_n_times' count=grantCount}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if userBadges}}
|
||||
<div class={{unbound layoutClass}}>
|
||||
{{#each ub in userBadges}}
|
||||
<div class="badge-user">
|
||||
{{#link-to 'user' ub.user classNames="badge-info"}}
|
||||
{{avatar ub.user imageSize="large"}}
|
||||
<div class="details">
|
||||
<span class="username">{{ub.user.username}}</span>
|
||||
{{format-date ub.granted_at}}
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{#if user}}
|
||||
{{format-date ub.granted_at}}
|
||||
{{else}}
|
||||
{{#link-to 'user' ub.user classNames="badge-info"}}
|
||||
{{avatar ub.user imageSize="large"}}
|
||||
<div class="details">
|
||||
<span class="username">{{ub.user.username}}</span>
|
||||
{{format-date ub.granted_at}}
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
|
||||
{{#if ub.post_number}}
|
||||
<a class="post-link" href="{{unbound ub.topic.url}}/{{unbound ub.post_number}}">{{{ub.topic.fancyTitle}}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#unless canLoadMore}}
|
||||
{{#if user}}
|
||||
<a class='load-more' href='{{model.url}}'>{{i18n 'badges.more_with_badge'}}</a>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
</div>
|
||||
|
||||
{{conditional-loading-spinner condition=canLoadMore}}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<input class="date-picker">
|
|
@ -20,18 +20,13 @@
|
|||
</section>
|
||||
|
||||
{{#if emailInEnabled}}
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.contains_messages}}
|
||||
{{i18n 'category.contains_messages'}}
|
||||
</label>
|
||||
</section>
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.email_in_allow_strangers}}
|
||||
{{i18n 'category.email_in_allow_strangers'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{fa-icon "envelope-o"}}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div id='period-popup' {{bind-attr class="showPeriods::hidden :period-popup"}}>
|
||||
<ul>
|
||||
{{#each p in site.periods}}
|
||||
<li><a href {{action "changePeriod" p}}>{{period-title p}}</a></li>
|
||||
<li><a href {{action "changePeriod" p}}>{{period-title p showDateRange=true}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#link-to 'badges.show' badge}}
|
||||
<a href="{{badgeUrl}}">
|
||||
{{#badge-button badge=badge}}
|
||||
{{#if showGrantCount}}
|
||||
<span class="count">(× {{count}})</span>
|
||||
{{/if}}
|
||||
{{/badge-button}}
|
||||
{{/link-to}}
|
||||
</a>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div class="container">
|
||||
<div class="container user-table">
|
||||
<div class="wrapper">
|
||||
<section class='user-navigation'>
|
||||
<ul class='action-list nav-stacked'>
|
||||
{{#each tabs as |tab|}}
|
||||
|
@ -22,4 +23,5 @@
|
|||
{{outlet}}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
{{#if model.finished}}
|
||||
{{d-button class="btn-primary" action="closeModal" label="close"}}
|
||||
{{else}}
|
||||
{{d-button icon="envelope" action="createInvite" class="btn-primary" disabled=disabled label=buttonTitle}}
|
||||
{{d-button icon=inviteIcon action="createInvite" class="btn-primary" disabled=disabled label=buttonTitle}}
|
||||
{{#if showCopyInviteButton}}
|
||||
{{d-button icon="link" action="generateInvitelink" class="btn-primary" disabled=disabledCopyLink label='user.invited.generate_link'}}
|
||||
{{/if}}
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
{{#if showBadges}}
|
||||
<div class="badge-section">
|
||||
{{#each ub in user.featured_user_badges}}
|
||||
{{user-badge badge=ub.badge}}
|
||||
{{user-badge badge=ub.badge user=user}}
|
||||
{{/each}}
|
||||
{{#if showMoreBadges}}
|
||||
{{#link-to 'user.badges' user class="btn more-user-badges"}}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<section class='user-navigation'>
|
||||
<ul class='action-list nav-stacked'>
|
||||
<ul class='action-list activity-list nav-stacked'>
|
||||
|
||||
<li class='no-glyph'>
|
||||
{{#link-to 'userActivity.index'}}{{i18n 'user.filters.all'}}{{/link-to}}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<section class='user-content user-badges-list'>
|
||||
{{#each ub in controller}}
|
||||
{{user-badge badge=ub.badge count=ub.count}}
|
||||
{{user-badge badge=ub.badge count=ub.count user=user}}
|
||||
{{/each}}
|
||||
</section>
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
<section class='user-navigation'>
|
||||
{{#unless mobileView}}
|
||||
{{#if showNewPM}}
|
||||
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
<ul class='action-list nav-stacked'>
|
||||
<li class="noGlyph">
|
||||
{{#link-to 'userPrivateMessages.index' model}}
|
||||
|
@ -23,7 +28,7 @@
|
|||
{{capitalize group.name}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
<li class='archive'>
|
||||
{{#link-to 'userPrivateMessages.groupArchive' group.name}}
|
||||
{{i18n 'user.messages.archive'}}
|
||||
{{/link-to}}
|
||||
|
@ -32,7 +37,6 @@
|
|||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
|
||||
</section>
|
||||
|
||||
<section class='user-right messages'>
|
||||
|
@ -42,6 +46,12 @@
|
|||
<i class="fa fa-list"></i>
|
||||
</button>
|
||||
|
||||
{{#if mobileView}}
|
||||
{{#if showNewPM}}
|
||||
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canArchive}}
|
||||
<button {{action "archive"}} class="btn btn-archive">
|
||||
{{i18n "user.messages.archive"}}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
<section class='user-navigation'>
|
||||
<ul class='action-list nav-stacked'>
|
||||
<ul class='notification-list action-list nav-stacked'>
|
||||
{{#if model}}
|
||||
<li class='no-glyph'>
|
||||
{{#link-to 'userNotifications.index'}}{{i18n 'user.filters.all'}}{{/link-to}}
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
{{/if}}
|
||||
{{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}}
|
||||
{{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}}
|
||||
{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}
|
||||
<span class="pref-mailing-list-mode">{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}</span>
|
||||
{{preference-checkbox labelKey="user.email_always" checked=model.email_always}}
|
||||
|
||||
<div class='instructions'>
|
||||
|
|
62
app/assets/javascripts/discourse/templates/user/summary.hbs
Normal file
62
app/assets/javascripts/discourse/templates/user/summary.hbs
Normal file
|
@ -0,0 +1,62 @@
|
|||
{{#if model.replies.length}}
|
||||
<div class='top-section'>
|
||||
<h3>{{i18n "user.summary.top_replies"}}</h3>
|
||||
{{#each reply in model.replies}}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{reply.url}}">{{reply.topic.title}}</a> {{#if reply.like_count}}<span class='like-count'>{{reply.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date reply.createdAt format="tiny" noTitle="true"}}
|
||||
</li>
|
||||
</ul>
|
||||
{{/each}}
|
||||
{{#if moreReplies}}
|
||||
{{#link-to "userActivity.replies" user class="more"}}{{i18n "user.summary.more_replies"}}{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.topics.length}}
|
||||
<div class='top-section'>
|
||||
<h3>{{i18n "user.summary.top_topics"}}</h3>
|
||||
{{#each topic in model.topics}}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{topic.url}}">{{topic.title}}</a> {{#if topic.like_count}}<span class='like-count'>{{topic.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date topic.createdAt format="tiny" noTitle="true"}}
|
||||
</li>
|
||||
</ul>
|
||||
{{/each}}
|
||||
{{#if moreTopics}}
|
||||
{{#link-to "userActivity.topics" user class="more"}}{{i18n "user.summary.more_topics"}}{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='top-section stats-section'>
|
||||
<h3>{{i18n "user.summary.stats"}}</h3>
|
||||
<dl>
|
||||
<dt>{{i18n "user.summary.topic_count"}}</dt>
|
||||
<dd>{{model.topic_count}}</dd>
|
||||
<dt>{{i18n "user.summary.post_count"}}</dt>
|
||||
<dd>{{model.post_count}}</dd>
|
||||
<dt>{{i18n "user.summary.likes_given"}}</dt>
|
||||
<dd>{{model.likes_given}}</dd>
|
||||
<dt>{{i18n "user.summary.likes_received"}}</dt>
|
||||
<dd>{{model.likes_received}}</dd>
|
||||
<dt>{{i18n "user.summary.days_visited"}}</dt>
|
||||
<dd>{{model.days_visited}}</dd>
|
||||
<dt>{{i18n "user.summary.posts_read_count"}}</dt>
|
||||
<dd>{{model.posts_read_count}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{{#if model.badges.length}}
|
||||
<div class='top-section badges-section'>
|
||||
<h3>{{i18n "user.summary.top_badges"}}</h3>
|
||||
{{#each badge in model.badges}}
|
||||
{{user-badge badge=badge count=badge.count user=user}}
|
||||
{{/each}}
|
||||
{{#if moreBadges}}
|
||||
{{#link-to "user.badges" user class="more"}}{{i18n "user.summary.more_badges"}}{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user